Skip to content

feat(ui): 完善 AnimationService#2261

Open
Hill23333 wants to merge 26 commits intoPCL-Community:devfrom
Creative-Generator:feat/animation-replace
Open

feat(ui): 完善 AnimationService#2261
Hill23333 wants to merge 26 commits intoPCL-Community:devfrom
Creative-Generator:feat/animation-replace

Conversation

@Hill23333
Copy link
Copy Markdown
Contributor

@Hill23333 Hill23333 commented Jan 19, 2026

Summary by Sourcery

修订动画基础设施,以支持更丰富的类型、命名、取消以及改进的缓动效果,并扩展 UI 值类型以简化与 WPF 的互操作。

新功能:

  • 在动画服务中引入具名动画,支持注册、冲突处理以及通过名称进行取消。
  • 新增对 NScaleTransformNRotateTransform 类型的支持,包括动画、值处理器以及 WPF 互操作转换。
  • 添加 ActionAnimation 及相关帧/可动画辅助工具,通过动画管线执行任意操作。
  • 提供 CompositeEasingCombinedEasingBackEaseWithPower 等缓动变体,并为现有缓动函数提供共享单例实例。
  • 使 NColor 支持通过资源键构造,并可以在颜色类型与画刷类型之间进行隐式转换。

增强:

  • 重构 AnimationService 的帧处理,以携带源动画、跳过已取消的帧,并通过逐帧操作驱动更新。
  • 通过 AnimationStatus 统一动画状态跟踪,用基于状态的逻辑替代 IsCompleted,并使 RunAsync/RunFireAndForget 返回动画实例。
  • 重做 FromToAnimationBase 和动画组,以使用线程安全的帧计数器、基于处理器的默认值以及更好的取消语义。
  • 扩展 ValueProcessor 基础设施,新增加法/缩放/默认值/相等性辅助工具,并在现有处理器中实现这些功能。
  • 改进 WpfAnimatableIAnimatable,以支持强类型 SetValue 和更丰富的 WPF 对象类型过滤。
Original summary in English

Summary by Sourcery

Revise the animation infrastructure to support richer types, naming, cancellation and improved easing, and extend UI value types to ease WPF interop.

New Features:

  • Introduce named animations with registration, conflict handling and cancellation by name in the animation service.
  • Add support for NScaleTransform and NRotateTransform types, including animations, value processors, and WPF interop conversions.
  • Add ActionAnimation and related frames/animatable helpers to execute arbitrary actions through the animation pipeline.
  • Provide CompositeEasing, CombinedEasing and BackEaseWithPower easing variants plus shared singleton instances for existing easing functions.
  • Enable NColor to construct from resource keys and to convert implicitly between color and brush types.

Enhancements:

  • Refactor AnimationService frame handling to carry source animations, skip canceled frames and drive updates via per-frame actions.
  • Unify animation status tracking via AnimationStatus, replace IsCompleted with state-based logic and make RunAsync/RunFireAndForget return animation instances.
  • Rework FromToAnimationBase and animation groups to use thread-safe frame counters, processor-based defaults, and better cancellation semantics.
  • Extend ValueProcessor infrastructure with add/scale/default/equality helpers and implement them across existing processors.
  • Improve WpfAnimatable and IAnimatable to support typed SetValue and richer type filtering for WPF objects.

@pcl-ce-automation pcl-ce-automation bot added 🚧 正在处理 开发人员正在对该内容进行开发、测试或修复,进展中 size: XXL PR 大小评估:巨型 labels Jan 19, 2026
@Hill23333 Hill23333 changed the title feat(ui): 将代码内 ModAnimation 的调用替换为 AnimationService feat(ui): 完善 AnimationService Apr 5, 2026
@Hill23333 Hill23333 marked this pull request as ready for review April 5, 2026 15:02
@pcl-ce-automation pcl-ce-automation bot removed the 🚧 正在处理 开发人员正在对该内容进行开发、测试或修复,进展中 label Apr 5, 2026
@pcl-ce-automation pcl-ce-automation bot added the 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查 label Apr 5, 2026
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 5, 2026

评审者指南

对动画系统进行重构与扩展,使其基于状态驱动和动作应用:新增具名及可取消动画支持、全新的变换与颜色抽象、更丰富的缓动工具,以及通用的数值处理机制,同时更新动画分组与帧以适配新的模型。

AnimationService 中更新后的动画生命周期序列图

sequenceDiagram
    actor XamlTrigger as XamlTrigger
    participant RunAction as RunAnimationAction
    participant Anim as FromToAnimationBase_T
    participant AnimService as AnimationService
    participant ComputeTask as AnimationComputeTask
    participant FrameChannel as FrameChannel
    participant UIThread as UIThread
    participant Target as IAnimatable

    XamlTrigger->>RunAction: Invoke()
    RunAction->>Anim: RunFireAndForget(Target)
    activate Anim
    Anim->>Anim: _RunCore(Target)
    Anim->>AnimService: PushAnimationFireAndForget(thisClone, Target)
    deactivate Anim

    rect rgb(235,235,255)
        note over AnimService: Handle named animation registration and conflict
        AnimService->>AnimService: HandleNamedAnimationConflict(animation)
        alt animation.Name exists in _namedAnimations
            AnimService->>OldAnim: Cancel()
            note over OldAnim: Status = Canceled
        end
        AnimService->>AnimService: _animationChannel.Writer.TryWrite((animation, target))
    end

    loop Background compute tasks
        ComputeTask->>AnimService: _animationChannel.Reader.TryRead()
        AnimService-->>ComputeTask: (animation, target)
        activate ComputeTask
        ComputeTask->>Anim: Status check
        alt Status is Canceled or Completed
            ComputeTask->>Anim: RaiseCompleted()
            alt animation.Name not empty
                ComputeTask->>AnimService: _namedAnimations.Remove(animation.Name)
            end
            ComputeTask->>ComputeTask: Remove from local list
        else Animation still Running
            ComputeTask->>Anim: ComputeNextFrame(target)
            alt frame is not null
                ComputeTask->>FrameChannel: Writer.TryWrite((frame, animation))
                ComputeTask->>Anim: CurrentFrame++
            end
        end
        ComputeTask->>ComputeTask: Wait _resetEvent (clock tick)
        deactivate ComputeTask
    end

    loop UI apply loop
        UIThread->>FrameChannel: Reader.WaitToReadAsync()
        FrameChannel-->>UIThread: (frame, animation)
        UIThread->>Anim: Status
        alt animation.Status == Canceled
            UIThread->>UIThread: discard frame
        else
            UIThread->>frame: GetAction()
            frame-->>UIThread: action
            UIThread->>UIThread: Invoke action()
            note over Target: SetValue(...) executed on UI thread
        end
    end

    note over AnimService: CancelAnimationByName(name) removes and cancels stored animation
Loading

核心动画模型更新后的类图

classDiagram
    class IAnimatable {
        +object GetValue()
        +void SetValue(object value)
        +void SetValue~T~(T value)
    }

    class EmptyAnimatable {
        +static EmptyAnimatable Instance
        +object GetValue()
        +void SetValue(object value)
        +void SetValue~T~(T value)
    }
    IAnimatable <|.. EmptyAnimatable

    class ClrAnimatable~T~ {
        -T _value
        +T GetValue()
        +void SetValue(T value)
        +object GetValue()
        +void SetValue(object value)
        +void SetValue~TValue~(TValue value)
    }
    IAnimatable <|.. ClrAnimatable

    class WpfAnimatable {
        +DependencyObject Owner
        +DependencyProperty Property
        +object GetValue()
        +void SetValue(object value)
        +void SetValue~T~(T value)
        -void SetValueCore~T~(T value)
    }
    IAnimatable <|.. WpfAnimatable

    class AnimationStatus {
        <<enum>>
        NotStarted
        Running
        Completed
        Canceled
    }

    class IAnimation {
        +string Name
        +AnimationStatus Status
        +int CurrentFrame
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +IAnimation RunFireAndForget(IAnimatable target)
        +void Cancel()
        +IAnimationFrame ComputeNextFrame(IAnimatable target)
        +event EventHandler Started
        +event EventHandler Completed
    }

    class AnimationBase {
        +string Name
        -int _status
        +AnimationStatus Status
        +int CurrentFrame
        +Task~IAnimation~ RunAsync(IAnimatable target)*
        +IAnimation RunFireAndForget(IAnimatable target)*
        +void Cancel()*
        +IAnimationFrame ComputeNextFrame(IAnimatable target)*
        +void RaiseStarted()
        +void RaiseCompleted()
    }
    IAnimation <|.. AnimationBase

    class FromToAnimationBase~T~ {
        +IEasing Easing
        +T From
        +T To
        +AnimationValueType ValueType
        +TimeSpan Duration
        +TimeSpan Delay
        +T CurrentValue
        +int TotalFrames
        -int _currentFrame
        -T _startValue
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +IAnimation RunFireAndForget(IAnimatable target)
        +void Cancel()
        +IAnimationFrame ComputeNextFrame(IAnimatable target)
        -void _RunCore(IAnimatable target)
    }
    AnimationBase <|-- FromToAnimationBase

    class ActionAnimation {
        +Action~CancellationToken~ Action
        +int CurrentFrame
        +TimeSpan Delay
        -CancellationTokenSource _cts
        -TaskCompletionSource _tcs
        -int _called
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +IAnimation RunFireAndForget(IAnimatable target)
        +void Cancel()
        +IAnimationFrame ComputeNextFrame(IAnimatable target)
    }
    AnimationBase <|-- ActionAnimation

    class AnimationGroup {
        +ObservableCollection~IAnimation~ Children
        +int CurrentFrame
        +List~IAnimation~ ChildrenCore
        +void Cancel()
        +void CancelAndClear()
        +IAnimationFrame ComputeNextFrame(IAnimatable target)
        #static IAnimatable ResolveTarget(IAnimation animation, IAnimatable defaultTarget)
        #static Task CreateChildAwaiter(IAnimation animation)
    }
    AnimationBase <|-- AnimationGroup

    class ParallelAnimationGroup {
        -TaskCompletionSource _cancelTcs
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +void Cancel()
        +IAnimation RunFireAndForget(IAnimatable target)
    }
    AnimationGroup <|-- ParallelAnimationGroup

    class SequentialAnimationGroup {
        -TaskCompletionSource _cancelTcs
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +void Cancel()
        +IAnimation RunFireAndForget(IAnimatable target)
    }
    AnimationGroup <|-- SequentialAnimationGroup

    class IAnimationFrame {
        +Action GetAction()
    }

    class FromToAnimationFrame~T~ {
        +IAnimatable Target
        +T Value
        +T StartValue
        +Action GetAction()
    }
    IAnimationFrame <|.. FromToAnimationFrame

    class ActionAnimationFrame {
        +Action Action
        +Action GetAction()
    }
    IAnimationFrame <|.. ActionAnimationFrame

    class AnimationService {
        -static Channel~(IAnimation,IAnimatable)~ _animationChannel
        -static Channel~(IAnimationFrame,IAnimation)~ _frameChannel
        -static ConcurrentDictionary~string,IAnimation~ _namedAnimations
        -static IClock _clock
        -static AsyncCountResetEvent _resetEvent
        -static CancellationTokenSource _cts
        +static int Fps
        +static double Scale
        +static IUIAccessProvider UIAccessProvider
        +void Start()
        +void Stop()
        -static void _Initialize()
        -static void _Uninitialize()
        -static Task _AnimationComputeTaskAsync()
        -static void HandleNamedAnimationConflict(IAnimation animation)
        +static Task PushAnimationAsync(IAnimation animation, IAnimatable target)
        +static void PushAnimationFireAndForget(IAnimation animation, IAnimatable target)
        +static void CancelAnimationByName(string name)
    }

    class RunAnimationAction {
        +IAnimation Animation
        +DependencyProperty TargetProperty
        +void Invoke(object parameter)
    }

    RunAnimationAction --> IAnimation : uses
    FromToAnimationBase ..> IAnimatable : target
    ActionAnimation ..> IAnimatable : target
    AnimationGroup o--> IAnimation : Children
    ParallelAnimationGroup ..> AnimationService : PushAnimationFireAndForget
    SequentialAnimationGroup ..> AnimationService : PushAnimationFireAndForget
    AnimationService o--> IAnimation : manages
    AnimationService o--> IAnimatable : targets
    AnimationService o--> IAnimationFrame : processes
    FromToAnimationFrame ..> ValueProcessorManager
    WpfAnimatable ..> NColor
    WpfAnimatable ..> NScaleTransform
    WpfAnimatable ..> NRotateTransform
Loading

数值处理与新 UI 数值类型的类图

classDiagram
    class IValueProcessor~T~ {
        +T Filter(T value)
        +T Add(T value1, T value2)
        +T Subtract(T value1, T value2)
        +T Scale(T value, double factor)
        +T DefaultValue()
        +bool Equal(T value1, T value2)
    }

    class ValueProcessorManager {
        -Dictionary~Type,Func~object,object~~ Filters
        -Dictionary~Type,Func~object,object,object~~ Adders
        -Dictionary~Type,Func~object,double,object~~ Scalers
        +void Register~T~(IValueProcessor~T~ processor)
        +T Filter~T~(T value)
        +object Filter(object value)
        +T Add~T~(T value1, T value2)
        +object Add(object value1, object value2)
        +T Subtract~T~(T value1, T value2)
        +T Scale~T~(T value, double factor)
        +object Scale(object value, double factor)
        +T DefaultValue~T~()
        +bool Equal~T~(T value1, T value2)
    }

    IValueProcessor <|.. DoubleValueProcessor
    class DoubleValueProcessor {
        +double Filter(double value)
        +double Add(double value1, double value2)
        +double Subtract(double value1, double value2)
        +double Scale(double value, double factor)
        +double DefaultValue()
        +bool Equal(double value1, double value2)
    }

    IValueProcessor <|.. NColorValueProcessor
    class NColorValueProcessor {
        +NColor Filter(NColor value)
        +NColor Add(NColor value1, NColor value2)
        +NColor Subtract(NColor value1, NColor value2)
        +NColor Scale(NColor value, double factor)
        +NColor DefaultValue()
        +bool Equal(NColor value1, NColor value2)
    }

    IValueProcessor <|.. ThicknessValueProcessor
    class ThicknessValueProcessor {
        +Thickness Filter(Thickness value)
        +Thickness Add(Thickness value1, Thickness value2)
        +Thickness Subtract(Thickness value1, Thickness value2)
        +Thickness Scale(Thickness value, double factor)
        +Thickness DefaultValue()
        +bool Equal(Thickness value1, Thickness value2)
    }

    IValueProcessor <|.. MatrixValueProcessor
    class MatrixValueProcessor {
        +Matrix Filter(Matrix value)
        +Matrix Add(Matrix value1, Matrix value2)
        +Matrix Subtract(Matrix value1, Matrix value2)
        +Matrix Scale(Matrix value, double factor)
        +Matrix DefaultValue()
        +bool Equal(Matrix value1, Matrix value2)
    }

    IValueProcessor <|.. PointValueProcessor
    class PointValueProcessor {
        +Point Filter(Point value)
        +Point Add(Point value1, Point value2)
        +Point Subtract(Point value1, Point value2)
        +Point Scale(Point value, double factor)
        +Point DefaultValue()
        +bool Equal(Point value1, Point value2)
    }

    IValueProcessor <|.. NScaleTransformValueProcessor
    class NScaleTransformValueProcessor {
        +NScaleTransform Filter(NScaleTransform value)
        +NScaleTransform Add(NScaleTransform value1, NScaleTransform value2)
        +NScaleTransform Subtract(NScaleTransform value1, NScaleTransform value2)
        +NScaleTransform Scale(NScaleTransform value, double factor)
        +NScaleTransform DefaultValue()
        +bool Equal(NScaleTransform value1, NScaleTransform value2)
    }

    IValueProcessor <|.. NRotateTransformValueProcessor
    class NRotateTransformValueProcessor {
        +NRotateTransform Filter(NRotateTransform value)
        +NRotateTransform Add(NRotateTransform value1, NRotateTransform value2)
        +NRotateTransform Subtract(NRotateTransform value1, NRotateTransform value2)
        +NRotateTransform Scale(NRotateTransform value, double factor)
        +NRotateTransform DefaultValue()
        +bool Equal(NRotateTransform value1, NRotateTransform value2)
    }

    ValueProcessorManager o--> IValueProcessor

    class NColor {
        -Vector4 _color
        +float R
        +float G
        +float B
        +float A
        +NColor()
        +NColor(float r, float g, float b, float a)
        +NColor(Color color)
        +NColor(SystemDrawingColor color)
        +NColor(string str)
        +NColor(Brush brush)
        +NColor(SolidColorBrush brush)
        +NColor op_Add(NColor a, NColor b)
        +NColor op_Subtract(NColor a, NColor b)
        +NColor op_Multiply(NColor a, float b)
        +NColor op_Division(NColor a, float b)
        +bool op_Equality(NColor a, NColor b)
        +bool op_Inequality(NColor a, NColor b)
        +Color op_Implicit(NColor color)
        +Brush op_Implicit(NColor color)
        +SolidColorBrush op_Implicit(NColor color)
        +NColor op_Implicit(Color color)
        +NColor op_Implicit(Brush brush)
        +NColor op_Implicit(SolidColorBrush brush)
    }

    class NScaleTransform {
        -Vector4 _scale
        +float ScaleX
        +float ScaleY
        +float CenterX
        +float CenterY
        +NScaleTransform()
        +NScaleTransform(float scaleX, float scaleY, float centerX, float centerY)
        +NScaleTransform(ScaleTransform scaleTransform)
        +NScaleTransform op_Add(NScaleTransform a, NScaleTransform b)
        +NScaleTransform op_Subtract(NScaleTransform a, NScaleTransform b)
        +NScaleTransform op_Multiply(NScaleTransform a, float b)
        +NScaleTransform op_Division(NScaleTransform a, float b)
        +bool op_Equality(NScaleTransform a, NScaleTransform b)
        +bool op_Inequality(NScaleTransform a, NScaleTransform b)
        +ScaleTransform op_Implicit(NScaleTransform st)
        +NScaleTransform op_Implicit(ScaleTransform st)
    }

    class NRotateTransform {
        -Vector3 _rotate
        +float Angle
        +float CenterX
        +float CenterY
        +NRotateTransform()
        +NRotateTransform(float angle, float centerX, float centerY)
        +NRotateTransform(RotateTransform rotateTransform)
        +NRotateTransform op_Add(NRotateTransform a, NRotateTransform b)
        +NRotateTransform op_Subtract(NRotateTransform a, NRotateTransform b)
        +NRotateTransform op_Multiply(NRotateTransform a, float b)
        +NRotateTransform op_Division(NRotateTransform a, float b)
        +bool op_Equality(NRotateTransform a, NRotateTransform b)
        +bool op_Inequality(NRotateTransform a, NRotateTransform b)
        +RotateTransform op_Implicit(NRotateTransform rt)
        +NRotateTransform op_Implicit(RotateTransform rt)
    }

    NColorValueProcessor ..> NColor
    NScaleTransformValueProcessor ..> NScaleTransform
    NRotateTransformValueProcessor ..> NRotateTransform
    WpfAnimatable ..> NColor : converts
    WpfAnimatable ..> NScaleTransform : converts
    WpfAnimatable ..> NRotateTransform : converts

    class IEasing {
        +double Ease(double progress)
        +double Ease(int currentFrame, int totalFrames)
    }

    class Easing {
        +double Ease(double progress)
        +double Ease(int currentFrame, int totalFrames)
        #double EaseCore(double progress)*
    }
    IEasing <|.. Easing

    class CompositeEasing {
        -List~(IEasing,TimeSpan,TimeSpan,double)~ _easings
        -TimeSpan _totalDuration
        +CompositeEasing((IEasing,TimeSpan)[] easings)
        +CompositeEasing((IEasing,TimeSpan,TimeSpan)[] easings)
        +CompositeEasing((IEasing,TimeSpan,double)[] easings)
        +CompositeEasing((IEasing,TimeSpan,TimeSpan,double)[] easings)
        +TimeSpan TotalDuration
        #double EaseCore(double progress)
    }
    Easing <|-- CompositeEasing

    class CombinedEasing {
        -IEasing _ease1
        -IEasing _ease2
        -double _split
        #double EaseCore(double t)
    }
    Easing <|-- CombinedEasing

    class BackEaseWithPowerIn {
        -double _p
        #double EaseCore(double progress)
    }
    Easing <|-- BackEaseWithPowerIn

    class BackEaseWithPowerOut {
        -double _p
        #double EaseCore(double progress)
    }
    Easing <|-- BackEaseWithPowerOut

    class BackEaseWithPowerInOut {
        -double _p
        #double EaseCore(double progress)
    }
    Easing <|-- BackEaseWithPowerInOut

    class EasePower {
        <<enum>>
        Weak
        Middle
        Strong
        ExtraStrong
    }

    CompositeEasing ..> IEasing
    CombinedEasing ..> IEasing
    BackEaseWithPowerIn ..> EasePower
    BackEaseWithPowerOut ..> EasePower
    BackEaseWithPowerInOut ..> EasePower
Loading

文件级变更

Change Details Files
将 AnimationService 重构为具备状态感知能力,在帧之间携带动画源,并支持具名/可取消动画。
  • 将动画/帧通道改为携带包含动画源的强类型元组,并在 UI 线程上忽略来自已取消动画的帧。
  • 在动画上引入 AnimationStatus,并在计算循环中利用该状态移除已完成/已取消的动画,同时触发完成事件并清理具名动画注册。
  • 新增 ConcurrentDictionary 用于按名称注册动画,通过取消/替换现有动画处理命名冲突,并暴露 CancelAnimationByName 以及共享的 Scale/Fps 配置更新接口。
  • 在初始化时为旋转和缩放变换注册新的数值处理器,并在反初始化时清空动画注册表。
PCL.Core/UI/Animation/Core/AnimationService.cs
PCL.Core/UI/Animation/Core/AnimationStatus.cs
围绕状态、克隆以及基于动作的帧应用,重新设计动画核心 API(IAnimation、AnimationBase、FromToAnimationBase、帧以及 animatable 实现)。
  • 为 IAnimation 增加 Name 和 Status,修改 RunAsync/RunFireAndForget 以返回正在运行的 IAnimation 实例,并在 AnimationBase 中通过 AnimationStatus 实现线程安全的 Status。
  • 重构 FromToAnimationBase:要求非空 From,To 可选;默认采用绝对 ValueType;CurrentFrame 线程安全;每次运行采用“克隆后运行”的语义,将克隆推送到 AnimationService;ComputeNextFrame 现在构造 FromToAnimationFrame 并正确设置 Status。
  • 用 FromToAnimationFrame 替换 AnimationFrame,使其封装目标并返回一个用于应用数值的 Action,同时将 IAnimationFrame 改为只暴露 GetAction,而不再提供直接的数值访问器。
  • 增强 IAnimatable(以及 ClrAnimatable/WpfAnimatable 实现),支持泛型 SetValue、对 NColor/NScaleTransform/NRotateTransform 的转换处理,以及通过 ValueProcessorManager 进行过滤;并新增 EmptyAnimatable 用于无目标动画。
PCL.Core/UI/Animation/Core/IAnimation.cs
PCL.Core/UI/Animation/Core/AnimationBase.cs
PCL.Core/UI/Animation/Core/FromToAnimationBase.cs
PCL.Core/UI/Animation/Core/FromToAnimationFrame.cs
PCL.Core/UI/Animation/Core/IAnimationFrame.cs
PCL.Core/UI/Animation/Core/AnimationFrame.cs
PCL.Core/UI/Animation/Animatable/IAnimatable.cs
PCL.Core/UI/Animation/Animatable/ClrAnimatable.cs
PCL.Core/UI/Animation/Animatable/WpfAnimatable.cs
PCL.Core/UI/Animation/Animatable/EmptyAnimatable.cs
重新实现 SequentialAnimationGroup 与 ParallelAnimationGroup,使其通过动画服务运行,显式管理子实例,并支持协作式取消。
  • 修改 AnimationGroup,使其拥有运行中子实例的 ChildrenCore 列表,提供 Cancel 逻辑以取消并清空 ChildrenCore,同时新增 CancelAndClear 以及辅助方法 ResolveTarget 和 CreateChildAwaiter。
  • 更新 ParallelAnimationGroup.RunAsync:将自身推送到 AnimationService,立即启动所有子动画(RunFireAndForget),在 ChildrenCore 中追踪实例,等待它们全部完成或取消 TCS 完成,然后返回该分组;实现 Cancel 用于发出取消信号,并让 RunFireAndForget 委托给 RunAsync。
  • 更新 SequentialAnimationGroup.RunAsync:将自身推送到 AnimationService,使用 RunFireAndForget 逐个运行子动画,并通过每个子动画的等待器和取消 TCS 串行调度,确保正确更新 Status;重写 Cancel 以发出取消信号,并让 RunFireAndForget 返回该分组本身。
PCL.Core/UI/Animation/Core/AnimationGroup.cs
PCL.Core/UI/Animation/Core/ParallelAnimationGroup.cs
PCL.Core/UI/Animation/Core/SequentialAnimationGroup.cs
扩展数值处理能力:支持默认值、相等性检查、非泛型加法/缩放以及新的变换处理器,并相应调整使用方。
  • 更新 IValueProcessor,新增 DefaultValue 和 Equal 操作,并在 Double、NColor、Thickness、Matrix、Point 等处理器中实现这些方法。
  • 增强 ValueProcessorManager,新增用于过滤、加法、缩放的字典;增加非泛型 Add/Scale、DefaultValue 和 Equal 辅助方法,以供动画代码使用。
  • 引入 NScaleTransformValueProcessor 和 NRotateTransformValueProcessor,实现扩展后的接口以支持新的变换类型,并在 AnimationService 初始化时完成注册。
  • 更新 FromToAnimationBase 及具体的 FromTo 动画(Matrix/Point/Thickness,以及新的 NScale/NRotate 变体),改为依赖 ValueProcessorManager.Add/Scale/Subtract 和 DefaultValue/Equal 语义来实现相对/绝对插值。
PCL.Core/UI/Animation/ValueProcessor/IValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/ValueProcessorManager.cs
PCL.Core/UI/Animation/ValueProcessor/DoubleValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/NColorValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/ThicknessValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/MatrixValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/PointValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/NScaleTransformValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/NRotateTransformValueProcessor.cs
PCL.Core/UI/Animation/MatrixFromToAnimation.cs
PCL.Core/UI/Animation/PointFromToAnimation.cs
PCL.Core/UI/Animation/ThicknessFromToAnimation.cs
PCL.Core/UI/Animation/NScaleTransformFromToAnimation.cs
PCL.Core/UI/Animation/NRotateTransformFromToAnimation.cs
引入新的数值变换类型,并强化 NColor 以更好地集成资源、向量运算和隐式转换。
  • 新增 NScaleTransform 和 NRotateTransform 结构体,内部使用 Vector 类型存储数据,支持算术运算符和相等性比较,并提供与 WPF ScaleTransform/RotateTransform 的隐式互转,同时利用 AnimationService.UIAccessProvider 安全读取绑定到 UI 线程的属性。
  • 更新 WpfAnimatable.GetValue/SetValue,使其能够在运行时和通过泛型快速路径(使用 Unsafe 强制转换)在 WPF Color/Brush/Transform 与新的 N* 抽象之间映射。
  • 扩展 NColor 的构造函数和运算符:支持通过资源键构造(Color/SolidColorBrush 查找),在基于向量的构造函数中停止钳制,利用 Vector4 数学简化运算符实现,并新增与 Color/Brush/SolidColorBrush 的隐式双向转换。
  • 调整 AnimationService 的初始化逻辑以注册新的变换处理器,并微调描述文本和默认 Scale 系数。
PCL.Core/UI/NScaleTransform.cs
PCL.Core/UI/NRotateTransform.cs
PCL.Core/UI/NColor.cs
PCL.Core/UI/Animation/Animatable/WpfAnimatable.cs
PCL.Core/UI/Animation/Core/AnimationService.cs
新增更高层级的缓动工具,以及基于动作的动画类型,用于在动画管线中执行任意工作。
  • 通过 Shared 静态属性引入多个缓动单例,以避免重复分配,并调整 Easing.Ease(int,int) 使其在 totalFrames-1 上归一化,确保最后一帧总是映射到进度 1。
  • 新增 CompositeEasing,用于按时间权重组合多个缓动(可选延迟和权重);新增 CombinedEasing,用于带分割点的双阶段缓动;以及基于 EasePower 参数化强度的 BackEaseWithPowerIn/Out/InOut 变体。
  • 定义 EasePower 枚举,用于配置回弹类缓动的力度。
  • 新增 ActionAnimation 和 ActionAnimationFrame,通过 AnimationService 在动画管线中运行任意动作(支持可选延迟和取消令牌),并基于统一的 IAnimation/IAnimationFrame 流水线和状态模型实现。
  • 将 RunAnimation 触发器重命名为 RunAnimationAction 以更好地反映其行为,并更新 WPF 元数据类型。
PCL.Core/UI/Animation/Easings/Easing.cs
PCL.Core/UI/Animation/Easings/*.cs
PCL.Core/UI/Animation/Core/ActionAnimation.cs
PCL.Core/UI/Animation/Core/ActionAnimationFrame.cs
PCL.Core/UI/Animation/Animatable/EmptyAnimatable.cs
PCL.Core/UI/Animation/Core/RunAnimation.cs
PCL.Core/UI/Animation/Easings/EasePower.cs
PCL.Core/UI/Animation/Easings/CompositeEasing.cs
PCL.Core/UI/Animation/Easings/CombinedEasing.cs
PCL.Core/UI/Animation/Easings/BackEaseWithPowerIn.cs
PCL.Core/UI/Animation/Easings/BackEaseWithPowerOut.cs
PCL.Core/UI/Animation/Easings/BackEaseWithPowerInOut.cs

技巧与命令

与 Sourcery 交互

  • 触发新评审: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的评审评论。
  • 从评审评论生成 GitHub issue: 在评审评论下回复,请求 Sourcery 根据该评论创建 issue。你也可以直接回复 @sourcery-ai issue 来从该评论创建 issue。
  • 生成 pull request 标题: 在 pull request 标题的任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成评审者指南: 在 pull request 中评论 @sourcery-ai guide,即可随时(重新)生成评审者指南。
  • 一次性解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,即可将所有 Sourcery 评论标记为已解决。如果你已经处理完所有评论且不希望继续看到它们,这会很有用。
  • 忽略所有 Sourcery 评审: 在 pull request 中评论 @sourcery-ai dismiss,即可忽略所有现有的 Sourcery 评审。若你希望从头开始一次新的评审,这尤其有用——别忘了随后再评论 @sourcery-ai review 触发新的评审!

自定义你的使用体验

访问你的 控制面板 以:

  • 启用或禁用评审特性,例如 Sourcery 自动生成的 pull request 摘要、评审者指南等。
  • 修改评审语言。
  • 添加、删除或编辑自定义评审指令。
  • 调整其他评审设置。

获取帮助

Original review guide in English

Reviewer's Guide

Refactors and extends the animation system to be status-driven and action-based, adds support for named and cancellable animations, new transform and color abstractions, richer easing utilities, and generalized value processing, while updating animation groups and frames to work with the new model.

Sequence diagram for the updated animation lifecycle in AnimationService

sequenceDiagram
    actor XamlTrigger as XamlTrigger
    participant RunAction as RunAnimationAction
    participant Anim as FromToAnimationBase_T
    participant AnimService as AnimationService
    participant ComputeTask as AnimationComputeTask
    participant FrameChannel as FrameChannel
    participant UIThread as UIThread
    participant Target as IAnimatable

    XamlTrigger->>RunAction: Invoke()
    RunAction->>Anim: RunFireAndForget(Target)
    activate Anim
    Anim->>Anim: _RunCore(Target)
    Anim->>AnimService: PushAnimationFireAndForget(thisClone, Target)
    deactivate Anim

    rect rgb(235,235,255)
        note over AnimService: Handle named animation registration and conflict
        AnimService->>AnimService: HandleNamedAnimationConflict(animation)
        alt animation.Name exists in _namedAnimations
            AnimService->>OldAnim: Cancel()
            note over OldAnim: Status = Canceled
        end
        AnimService->>AnimService: _animationChannel.Writer.TryWrite((animation, target))
    end

    loop Background compute tasks
        ComputeTask->>AnimService: _animationChannel.Reader.TryRead()
        AnimService-->>ComputeTask: (animation, target)
        activate ComputeTask
        ComputeTask->>Anim: Status check
        alt Status is Canceled or Completed
            ComputeTask->>Anim: RaiseCompleted()
            alt animation.Name not empty
                ComputeTask->>AnimService: _namedAnimations.Remove(animation.Name)
            end
            ComputeTask->>ComputeTask: Remove from local list
        else Animation still Running
            ComputeTask->>Anim: ComputeNextFrame(target)
            alt frame is not null
                ComputeTask->>FrameChannel: Writer.TryWrite((frame, animation))
                ComputeTask->>Anim: CurrentFrame++
            end
        end
        ComputeTask->>ComputeTask: Wait _resetEvent (clock tick)
        deactivate ComputeTask
    end

    loop UI apply loop
        UIThread->>FrameChannel: Reader.WaitToReadAsync()
        FrameChannel-->>UIThread: (frame, animation)
        UIThread->>Anim: Status
        alt animation.Status == Canceled
            UIThread->>UIThread: discard frame
        else
            UIThread->>frame: GetAction()
            frame-->>UIThread: action
            UIThread->>UIThread: Invoke action()
            note over Target: SetValue(...) executed on UI thread
        end
    end

    note over AnimService: CancelAnimationByName(name) removes and cancels stored animation
Loading

Updated class diagram for core animation model

classDiagram
    class IAnimatable {
        +object GetValue()
        +void SetValue(object value)
        +void SetValue~T~(T value)
    }

    class EmptyAnimatable {
        +static EmptyAnimatable Instance
        +object GetValue()
        +void SetValue(object value)
        +void SetValue~T~(T value)
    }
    IAnimatable <|.. EmptyAnimatable

    class ClrAnimatable~T~ {
        -T _value
        +T GetValue()
        +void SetValue(T value)
        +object GetValue()
        +void SetValue(object value)
        +void SetValue~TValue~(TValue value)
    }
    IAnimatable <|.. ClrAnimatable

    class WpfAnimatable {
        +DependencyObject Owner
        +DependencyProperty Property
        +object GetValue()
        +void SetValue(object value)
        +void SetValue~T~(T value)
        -void SetValueCore~T~(T value)
    }
    IAnimatable <|.. WpfAnimatable

    class AnimationStatus {
        <<enum>>
        NotStarted
        Running
        Completed
        Canceled
    }

    class IAnimation {
        +string Name
        +AnimationStatus Status
        +int CurrentFrame
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +IAnimation RunFireAndForget(IAnimatable target)
        +void Cancel()
        +IAnimationFrame ComputeNextFrame(IAnimatable target)
        +event EventHandler Started
        +event EventHandler Completed
    }

    class AnimationBase {
        +string Name
        -int _status
        +AnimationStatus Status
        +int CurrentFrame
        +Task~IAnimation~ RunAsync(IAnimatable target)*
        +IAnimation RunFireAndForget(IAnimatable target)*
        +void Cancel()*
        +IAnimationFrame ComputeNextFrame(IAnimatable target)*
        +void RaiseStarted()
        +void RaiseCompleted()
    }
    IAnimation <|.. AnimationBase

    class FromToAnimationBase~T~ {
        +IEasing Easing
        +T From
        +T To
        +AnimationValueType ValueType
        +TimeSpan Duration
        +TimeSpan Delay
        +T CurrentValue
        +int TotalFrames
        -int _currentFrame
        -T _startValue
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +IAnimation RunFireAndForget(IAnimatable target)
        +void Cancel()
        +IAnimationFrame ComputeNextFrame(IAnimatable target)
        -void _RunCore(IAnimatable target)
    }
    AnimationBase <|-- FromToAnimationBase

    class ActionAnimation {
        +Action~CancellationToken~ Action
        +int CurrentFrame
        +TimeSpan Delay
        -CancellationTokenSource _cts
        -TaskCompletionSource _tcs
        -int _called
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +IAnimation RunFireAndForget(IAnimatable target)
        +void Cancel()
        +IAnimationFrame ComputeNextFrame(IAnimatable target)
    }
    AnimationBase <|-- ActionAnimation

    class AnimationGroup {
        +ObservableCollection~IAnimation~ Children
        +int CurrentFrame
        +List~IAnimation~ ChildrenCore
        +void Cancel()
        +void CancelAndClear()
        +IAnimationFrame ComputeNextFrame(IAnimatable target)
        #static IAnimatable ResolveTarget(IAnimation animation, IAnimatable defaultTarget)
        #static Task CreateChildAwaiter(IAnimation animation)
    }
    AnimationBase <|-- AnimationGroup

    class ParallelAnimationGroup {
        -TaskCompletionSource _cancelTcs
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +void Cancel()
        +IAnimation RunFireAndForget(IAnimatable target)
    }
    AnimationGroup <|-- ParallelAnimationGroup

    class SequentialAnimationGroup {
        -TaskCompletionSource _cancelTcs
        +Task~IAnimation~ RunAsync(IAnimatable target)
        +void Cancel()
        +IAnimation RunFireAndForget(IAnimatable target)
    }
    AnimationGroup <|-- SequentialAnimationGroup

    class IAnimationFrame {
        +Action GetAction()
    }

    class FromToAnimationFrame~T~ {
        +IAnimatable Target
        +T Value
        +T StartValue
        +Action GetAction()
    }
    IAnimationFrame <|.. FromToAnimationFrame

    class ActionAnimationFrame {
        +Action Action
        +Action GetAction()
    }
    IAnimationFrame <|.. ActionAnimationFrame

    class AnimationService {
        -static Channel~(IAnimation,IAnimatable)~ _animationChannel
        -static Channel~(IAnimationFrame,IAnimation)~ _frameChannel
        -static ConcurrentDictionary~string,IAnimation~ _namedAnimations
        -static IClock _clock
        -static AsyncCountResetEvent _resetEvent
        -static CancellationTokenSource _cts
        +static int Fps
        +static double Scale
        +static IUIAccessProvider UIAccessProvider
        +void Start()
        +void Stop()
        -static void _Initialize()
        -static void _Uninitialize()
        -static Task _AnimationComputeTaskAsync()
        -static void HandleNamedAnimationConflict(IAnimation animation)
        +static Task PushAnimationAsync(IAnimation animation, IAnimatable target)
        +static void PushAnimationFireAndForget(IAnimation animation, IAnimatable target)
        +static void CancelAnimationByName(string name)
    }

    class RunAnimationAction {
        +IAnimation Animation
        +DependencyProperty TargetProperty
        +void Invoke(object parameter)
    }

    RunAnimationAction --> IAnimation : uses
    FromToAnimationBase ..> IAnimatable : target
    ActionAnimation ..> IAnimatable : target
    AnimationGroup o--> IAnimation : Children
    ParallelAnimationGroup ..> AnimationService : PushAnimationFireAndForget
    SequentialAnimationGroup ..> AnimationService : PushAnimationFireAndForget
    AnimationService o--> IAnimation : manages
    AnimationService o--> IAnimatable : targets
    AnimationService o--> IAnimationFrame : processes
    FromToAnimationFrame ..> ValueProcessorManager
    WpfAnimatable ..> NColor
    WpfAnimatable ..> NScaleTransform
    WpfAnimatable ..> NRotateTransform
Loading

Class diagram for value processing and new UI value types

classDiagram
    class IValueProcessor~T~ {
        +T Filter(T value)
        +T Add(T value1, T value2)
        +T Subtract(T value1, T value2)
        +T Scale(T value, double factor)
        +T DefaultValue()
        +bool Equal(T value1, T value2)
    }

    class ValueProcessorManager {
        -Dictionary~Type,Func~object,object~~ Filters
        -Dictionary~Type,Func~object,object,object~~ Adders
        -Dictionary~Type,Func~object,double,object~~ Scalers
        +void Register~T~(IValueProcessor~T~ processor)
        +T Filter~T~(T value)
        +object Filter(object value)
        +T Add~T~(T value1, T value2)
        +object Add(object value1, object value2)
        +T Subtract~T~(T value1, T value2)
        +T Scale~T~(T value, double factor)
        +object Scale(object value, double factor)
        +T DefaultValue~T~()
        +bool Equal~T~(T value1, T value2)
    }

    IValueProcessor <|.. DoubleValueProcessor
    class DoubleValueProcessor {
        +double Filter(double value)
        +double Add(double value1, double value2)
        +double Subtract(double value1, double value2)
        +double Scale(double value, double factor)
        +double DefaultValue()
        +bool Equal(double value1, double value2)
    }

    IValueProcessor <|.. NColorValueProcessor
    class NColorValueProcessor {
        +NColor Filter(NColor value)
        +NColor Add(NColor value1, NColor value2)
        +NColor Subtract(NColor value1, NColor value2)
        +NColor Scale(NColor value, double factor)
        +NColor DefaultValue()
        +bool Equal(NColor value1, NColor value2)
    }

    IValueProcessor <|.. ThicknessValueProcessor
    class ThicknessValueProcessor {
        +Thickness Filter(Thickness value)
        +Thickness Add(Thickness value1, Thickness value2)
        +Thickness Subtract(Thickness value1, Thickness value2)
        +Thickness Scale(Thickness value, double factor)
        +Thickness DefaultValue()
        +bool Equal(Thickness value1, Thickness value2)
    }

    IValueProcessor <|.. MatrixValueProcessor
    class MatrixValueProcessor {
        +Matrix Filter(Matrix value)
        +Matrix Add(Matrix value1, Matrix value2)
        +Matrix Subtract(Matrix value1, Matrix value2)
        +Matrix Scale(Matrix value, double factor)
        +Matrix DefaultValue()
        +bool Equal(Matrix value1, Matrix value2)
    }

    IValueProcessor <|.. PointValueProcessor
    class PointValueProcessor {
        +Point Filter(Point value)
        +Point Add(Point value1, Point value2)
        +Point Subtract(Point value1, Point value2)
        +Point Scale(Point value, double factor)
        +Point DefaultValue()
        +bool Equal(Point value1, Point value2)
    }

    IValueProcessor <|.. NScaleTransformValueProcessor
    class NScaleTransformValueProcessor {
        +NScaleTransform Filter(NScaleTransform value)
        +NScaleTransform Add(NScaleTransform value1, NScaleTransform value2)
        +NScaleTransform Subtract(NScaleTransform value1, NScaleTransform value2)
        +NScaleTransform Scale(NScaleTransform value, double factor)
        +NScaleTransform DefaultValue()
        +bool Equal(NScaleTransform value1, NScaleTransform value2)
    }

    IValueProcessor <|.. NRotateTransformValueProcessor
    class NRotateTransformValueProcessor {
        +NRotateTransform Filter(NRotateTransform value)
        +NRotateTransform Add(NRotateTransform value1, NRotateTransform value2)
        +NRotateTransform Subtract(NRotateTransform value1, NRotateTransform value2)
        +NRotateTransform Scale(NRotateTransform value, double factor)
        +NRotateTransform DefaultValue()
        +bool Equal(NRotateTransform value1, NRotateTransform value2)
    }

    ValueProcessorManager o--> IValueProcessor

    class NColor {
        -Vector4 _color
        +float R
        +float G
        +float B
        +float A
        +NColor()
        +NColor(float r, float g, float b, float a)
        +NColor(Color color)
        +NColor(SystemDrawingColor color)
        +NColor(string str)
        +NColor(Brush brush)
        +NColor(SolidColorBrush brush)
        +NColor op_Add(NColor a, NColor b)
        +NColor op_Subtract(NColor a, NColor b)
        +NColor op_Multiply(NColor a, float b)
        +NColor op_Division(NColor a, float b)
        +bool op_Equality(NColor a, NColor b)
        +bool op_Inequality(NColor a, NColor b)
        +Color op_Implicit(NColor color)
        +Brush op_Implicit(NColor color)
        +SolidColorBrush op_Implicit(NColor color)
        +NColor op_Implicit(Color color)
        +NColor op_Implicit(Brush brush)
        +NColor op_Implicit(SolidColorBrush brush)
    }

    class NScaleTransform {
        -Vector4 _scale
        +float ScaleX
        +float ScaleY
        +float CenterX
        +float CenterY
        +NScaleTransform()
        +NScaleTransform(float scaleX, float scaleY, float centerX, float centerY)
        +NScaleTransform(ScaleTransform scaleTransform)
        +NScaleTransform op_Add(NScaleTransform a, NScaleTransform b)
        +NScaleTransform op_Subtract(NScaleTransform a, NScaleTransform b)
        +NScaleTransform op_Multiply(NScaleTransform a, float b)
        +NScaleTransform op_Division(NScaleTransform a, float b)
        +bool op_Equality(NScaleTransform a, NScaleTransform b)
        +bool op_Inequality(NScaleTransform a, NScaleTransform b)
        +ScaleTransform op_Implicit(NScaleTransform st)
        +NScaleTransform op_Implicit(ScaleTransform st)
    }

    class NRotateTransform {
        -Vector3 _rotate
        +float Angle
        +float CenterX
        +float CenterY
        +NRotateTransform()
        +NRotateTransform(float angle, float centerX, float centerY)
        +NRotateTransform(RotateTransform rotateTransform)
        +NRotateTransform op_Add(NRotateTransform a, NRotateTransform b)
        +NRotateTransform op_Subtract(NRotateTransform a, NRotateTransform b)
        +NRotateTransform op_Multiply(NRotateTransform a, float b)
        +NRotateTransform op_Division(NRotateTransform a, float b)
        +bool op_Equality(NRotateTransform a, NRotateTransform b)
        +bool op_Inequality(NRotateTransform a, NRotateTransform b)
        +RotateTransform op_Implicit(NRotateTransform rt)
        +NRotateTransform op_Implicit(RotateTransform rt)
    }

    NColorValueProcessor ..> NColor
    NScaleTransformValueProcessor ..> NScaleTransform
    NRotateTransformValueProcessor ..> NRotateTransform
    WpfAnimatable ..> NColor : converts
    WpfAnimatable ..> NScaleTransform : converts
    WpfAnimatable ..> NRotateTransform : converts

    class IEasing {
        +double Ease(double progress)
        +double Ease(int currentFrame, int totalFrames)
    }

    class Easing {
        +double Ease(double progress)
        +double Ease(int currentFrame, int totalFrames)
        #double EaseCore(double progress)*
    }
    IEasing <|.. Easing

    class CompositeEasing {
        -List~(IEasing,TimeSpan,TimeSpan,double)~ _easings
        -TimeSpan _totalDuration
        +CompositeEasing((IEasing,TimeSpan)[] easings)
        +CompositeEasing((IEasing,TimeSpan,TimeSpan)[] easings)
        +CompositeEasing((IEasing,TimeSpan,double)[] easings)
        +CompositeEasing((IEasing,TimeSpan,TimeSpan,double)[] easings)
        +TimeSpan TotalDuration
        #double EaseCore(double progress)
    }
    Easing <|-- CompositeEasing

    class CombinedEasing {
        -IEasing _ease1
        -IEasing _ease2
        -double _split
        #double EaseCore(double t)
    }
    Easing <|-- CombinedEasing

    class BackEaseWithPowerIn {
        -double _p
        #double EaseCore(double progress)
    }
    Easing <|-- BackEaseWithPowerIn

    class BackEaseWithPowerOut {
        -double _p
        #double EaseCore(double progress)
    }
    Easing <|-- BackEaseWithPowerOut

    class BackEaseWithPowerInOut {
        -double _p
        #double EaseCore(double progress)
    }
    Easing <|-- BackEaseWithPowerInOut

    class EasePower {
        <<enum>>
        Weak
        Middle
        Strong
        ExtraStrong
    }

    CompositeEasing ..> IEasing
    CombinedEasing ..> IEasing
    BackEaseWithPowerIn ..> EasePower
    BackEaseWithPowerOut ..> EasePower
    BackEaseWithPowerInOut ..> EasePower
Loading

File-Level Changes

Change Details Files
Refactor AnimationService to be status-aware, carry animation sources through frames, and support named/cancellable animations.
  • Change animation/frame channels to carry typed tuples including animation source, and ignore frames from canceled animations on the UI thread.
  • Introduce AnimationStatus on animations and use it in the compute loop to remove completed/canceled animations, raising completion and cleaning named animation registrations.
  • Add a ConcurrentDictionary to register animations by name, handle name conflicts by canceling/replacing existing animations, and expose CancelAnimationByName plus shared Scale/Fps configuration updates.
  • Register new value processors for rotation and scale transforms and clear animation registries on uninitialize.
PCL.Core/UI/Animation/Core/AnimationService.cs
PCL.Core/UI/Animation/Core/AnimationStatus.cs
Redesign the animation core API (IAnimation, AnimationBase, FromToAnimationBase, frames, and animatables) around status, cloning, and action-based frame application.
  • Extend IAnimation with Name and Status, switch RunAsync/RunFireAndForget to return the running IAnimation instance, and implement thread-safe Status via AnimationStatus in AnimationBase.
  • Rework FromToAnimationBase to use non-null From with optional To, absolute ValueType by default, thread-safe CurrentFrame, and clone-per-run semantics that push clones into AnimationService, with ComputeNextFrame now building FromToAnimationFrame and setting Status appropriately.
  • Replace AnimationFrame with FromToAnimationFrame that encapsulates the target and returns an Action to apply values, and change IAnimationFrame to expose GetAction instead of value accessors.
  • Enhance IAnimatable (and ClrAnimatable/WpfAnimatable implementations) with generic SetValue, conversion handling for NColor/NScaleTransform/NRotateTransform, and filtering via ValueProcessorManager, plus an EmptyAnimatable for non-targeted animations.
PCL.Core/UI/Animation/Core/IAnimation.cs
PCL.Core/UI/Animation/Core/AnimationBase.cs
PCL.Core/UI/Animation/Core/FromToAnimationBase.cs
PCL.Core/UI/Animation/Core/FromToAnimationFrame.cs
PCL.Core/UI/Animation/Core/IAnimationFrame.cs
PCL.Core/UI/Animation/Core/AnimationFrame.cs
PCL.Core/UI/Animation/Animatable/IAnimatable.cs
PCL.Core/UI/Animation/Animatable/ClrAnimatable.cs
PCL.Core/UI/Animation/Animatable/WpfAnimatable.cs
PCL.Core/UI/Animation/Animatable/EmptyAnimatable.cs
Reimplement SequentialAnimationGroup and ParallelAnimationGroup to run via the animation service, manage child instances explicitly, and support cooperative cancellation.
  • Change AnimationGroup to own a ChildrenCore list of running child instances, provide Cancel logic that cancels and clears ChildrenCore, and add CancelAndClear plus helper methods ResolveTarget and CreateChildAwaiter.
  • Update ParallelAnimationGroup.RunAsync to push the group into AnimationService, start all children immediately (RunFireAndForget), track instances in ChildrenCore, await their completion or a cancellation TCS, and return the group; implement Cancel to signal cancellations and RunFireAndForget to delegate to RunAsync.
  • Update SequentialAnimationGroup.RunAsync to push the group into AnimationService, run children one-by-one using RunFireAndForget with per-child awaiters and a cancellation TCS, and ensure Status is updated; override Cancel to signal cancellation and have RunFireAndForget return the group.
PCL.Core/UI/Animation/Core/AnimationGroup.cs
PCL.Core/UI/Animation/Core/ParallelAnimationGroup.cs
PCL.Core/UI/Animation/Core/SequentialAnimationGroup.cs
Extend value processing to support default values, equality, non-generic add/scale, and new transform processors, and adjust consumers accordingly.
  • Update IValueProcessor to include DefaultValue and Equal operations and implement these methods across Double, NColor, Thickness, Matrix, Point processors.
  • Enhance ValueProcessorManager with dictionaries for filters, adders, scalers; add non-generic Add/Scale, DefaultValue, and Equal helpers used by animation code.
  • Introduce NScaleTransformValueProcessor and NRotateTransformValueProcessor implementing the extended interface for new transform types and register them in AnimationService initialization.
  • Update FromToAnimationBase and concrete FromTo animations (Matrix/Point/Thickness, and new NScale/NRotate variants) to rely on ValueProcessorManager.Add/Scale/Subtract and DefaultValue/Equal semantics for relative/absolute interpolation.
PCL.Core/UI/Animation/ValueProcessor/IValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/ValueProcessorManager.cs
PCL.Core/UI/Animation/ValueProcessor/DoubleValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/NColorValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/ThicknessValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/MatrixValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/PointValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/NScaleTransformValueProcessor.cs
PCL.Core/UI/Animation/ValueProcessor/NRotateTransformValueProcessor.cs
PCL.Core/UI/Animation/MatrixFromToAnimation.cs
PCL.Core/UI/Animation/PointFromToAnimation.cs
PCL.Core/UI/Animation/ThicknessFromToAnimation.cs
PCL.Core/UI/Animation/NScaleTransformFromToAnimation.cs
PCL.Core/UI/Animation/NRotateTransformFromToAnimation.cs
Introduce new numeric transform types and bolster NColor to integrate with resources, vector math, and implicit conversions.
  • Add NScaleTransform and NRotateTransform structs that internally store data in Vector types, support arithmetic operators and equality, and provide implicit conversions to/from WPF ScaleTransform/RotateTransform, using AnimationService.UIAccessProvider to read UI-thread-bound properties safely.
  • Update WpfAnimatable.GetValue/SetValue to map between WPF Color/Brush/Transform types and the new N* abstractions both dynamically and via a generic fast path using Unsafe casts.
  • Extend NColor constructors and operators: allow constructing from resource keys (Color/SolidColorBrush lookups), stop clamping in vector-based constructor, simplify operators using Vector4 math, and add implicit conversions to/from Color/Brush/SolidColorBrush.
  • Adjust AnimationService initialization to register the new transform processors and tweak description text and default Scale factor.
PCL.Core/UI/NScaleTransform.cs
PCL.Core/UI/NRotateTransform.cs
PCL.Core/UI/NColor.cs
PCL.Core/UI/Animation/Animatable/WpfAnimatable.cs
PCL.Core/UI/Animation/Core/AnimationService.cs
Add higher-level easing utilities and an action-based animation type for arbitrary work in the animation pipeline.
  • Introduce many easing singletons via Shared static properties to avoid repeated allocations and adjust Easing.Ease(int,int) to normalize on totalFrames-1 so the last frame always maps to progress 1.
  • Add CompositeEasing for time-weighted combinations of multiple easings with optional delays and weights, CombinedEasing for two-phase easing with a split point, and new BackEaseWithPowerIn/Out/InOut variants parametrized by EasePower.
  • Define EasePower enum for configuring the strength of back easings.
  • Add ActionAnimation and ActionAnimationFrame to run arbitrary actions (with optional delay and cancellation tokens) through the AnimationService, implemented using the same IAnimation/IAnimationFrame pipeline and status model.
  • Rename RunAnimation trigger to RunAnimationAction to reflect behavior and update WPF metadata types.
PCL.Core/UI/Animation/Easings/Easing.cs
PCL.Core/UI/Animation/Easings/*.cs
PCL.Core/UI/Animation/Core/ActionAnimation.cs
PCL.Core/UI/Animation/Core/ActionAnimationFrame.cs
PCL.Core/UI/Animation/Animatable/EmptyAnimatable.cs
PCL.Core/UI/Animation/Core/RunAnimation.cs
PCL.Core/UI/Animation/Easings/EasePower.cs
PCL.Core/UI/Animation/Easings/CompositeEasing.cs
PCL.Core/UI/Animation/Easings/CombinedEasing.cs
PCL.Core/UI/Animation/Easings/BackEaseWithPowerIn.cs
PCL.Core/UI/Animation/Easings/BackEaseWithPowerOut.cs
PCL.Core/UI/Animation/Easings/BackEaseWithPowerInOut.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 3 个问题,并留下了一些总体反馈:

  • CircularEaseIn 中,Shared 静态属性的类型被写成了 CircularEaseOut 而不是 CircularEaseIn,这既会让调用方感到困惑,也会导致在不进行强制转换的情况下无法使用;请将属性类型更新为 CircularEaseIn 以与声明的类保持一致。
  • ActionAnimation.RunAsync/RunFireAndForget 的逻辑在使用 MemberwiseClone 的同时,将 _cts/_tcs 存储在原始实例上;AnimationService 使用的克隆实例会运行 action 并完成它自己的 _tcs,而 RunAsync 中等待的是原始实例的 _tcs,这可能导致其永远不完成——建议将 TCS/CTS 的创建移到克隆实例上,或者以其他方式确保 RunAsync 中等待的实例正是由 ComputeNextFrame 发出完成信号的那个实例。
  • NScaleTransform 中,+/-/*// 运算符只对 ScaleX/ScaleY 进行操作,忽略了 CenterX/CenterY,这在对中心点做动画时可能会出现令人意外的行为;请确认中心点是否也应该被插值/组合,并相应调整这些运算符(或者明确当前语义的文档)。
面向 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- In `CircularEaseIn` the `Shared` static property is typed as `CircularEaseOut` instead of `CircularEaseIn`, which will both confuse callers and prevent using it without a cast; update the property type to `CircularEaseIn` to match the declaring class.
- The `ActionAnimation.RunAsync`/`RunFireAndForget` logic uses `MemberwiseClone` while storing `_cts`/`_tcs` on the original instance; the clone used by `AnimationService` runs the action and completes its own `_tcs`, leaving the original `_tcs` awaited in `RunAsync` potentially never completing—consider moving the TCS/CTS creation to the clone or otherwise ensuring the instance awaited in `RunAsync` is the one whose completion is signaled from `ComputeNextFrame`.
- In `NScaleTransform` the `+`/`-`/`*`/`/` operators only operate on `ScaleX`/`ScaleY` and ignore `CenterX`/`CenterY`, which may lead to surprising behavior when animating centers; verify whether centers should also be interpolated/combined and adjust the operators (or document the current semantics) accordingly.

## Individual Comments

### Comment 1
<location path="PCL.Core/UI/Animation/Easings/CircularEaseIn.cs" line_range="7" />
<code_context>

 public class CircularEaseIn : Easing
 {
+    public static CircularEaseOut Shared { get; } = new();
+    
     protected override double EaseCore(double progress)
</code_context>
<issue_to_address>
**issue (bug_risk):** The `CircularEaseIn.Shared` singleton is declared with the wrong type.

`CircularEaseIn.Shared` is declared as `CircularEaseOut`, which appears to be a copy‑paste error and will cause incorrect typing/usage. It should be:

```csharp
public static CircularEaseIn Shared { get; } = new();
```
</issue_to_address>

### Comment 2
<location path="PCL.Core/UI/Animation/Core/FromToAnimationBase.cs" line_range="88-90" />
<code_context>

         // 如果 From 为空,则根据动画值类型设置初始值
-        From ??= ValueType == AnimationValueType.Relative ? default : _startValue;
+        if (!ValueProcessorManager.Equal(_startValue, From))
+        {
+           From = ValueType == AnimationValueType.Relative ? ValueProcessorManager.DefaultValue<T>() : _startValue;
+        }

</code_context>
<issue_to_address>
**issue (bug_risk):** The new `From` initialization logic overwrites explicit `From` values whenever they differ from the current target value.

Previously, `From` was only inferred when it was null/unspecified. With the new condition, any caller‑provided `From` that differs from `_startValue` is overwritten, so you can no longer animate from an arbitrary value.

If the goal is still “infer `From` only when it hasn’t been set”, consider instead checking for the unset/default case, e.g.:

```csharp
// if DefaultValue() represents "unset" for Relative animations
if (ValueProcessorManager.Equal(From, ValueProcessorManager.DefaultValue<T>()))
{
    From = ValueType == AnimationValueType.Relative
        ? ValueProcessorManager.DefaultValue<T>()
        : _startValue;
}
```

Alternatively, track an explicit `bool HasExplicitFrom` rather than inferring it via equality.
</issue_to_address>

### Comment 3
<location path="PCL.Core/UI/NScaleTransform.cs" line_range="79-81" />
<code_context>
+    
+    #region 运算符重载
+
+    public static NScaleTransform operator +(NScaleTransform a, NScaleTransform b) => new(a.ScaleX + b.ScaleX, a.ScaleY + b.ScaleY);
+    public static NScaleTransform operator -(NScaleTransform a, NScaleTransform b) => new(a.ScaleX - b.ScaleX, a.ScaleY - b.ScaleY);
+    public static NScaleTransform operator *(NScaleTransform a, float b) => new(a.ScaleX * b, a.ScaleY * b);
+
+    public static NScaleTransform operator /(NScaleTransform a, float b) =>
</code_context>
<issue_to_address>
**issue (bug_risk):** `NScaleTransform` arithmetic operators ignore `CenterX`/`CenterY`, which may lead to incorrect composite transforms.

The overloaded operators currently construct new instances with `CenterX`/`CenterY` implicitly set to 0, so center information is discarded during arithmetic and composition. If centers are meant to participate in interpolation/composition, the operators should also combine them, e.g.

```csharp
new(
    a.ScaleX + b.ScaleX,
    a.ScaleY + b.ScaleY,
    a.CenterX + b.CenterX,
    a.CenterY + b.CenterY)
```

`NRotateTransform`’s operators have the same issue: they modify only the angle and ignore the center.
</issue_to_address>

Sourcery 对开源免费——如果你喜欢这些代码审查,欢迎分享 ✨
帮我变得更有用!请在每条评论上点击 👍 或 👎,我会根据你的反馈改进后续的代码审查。
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • In CircularEaseIn the Shared static property is typed as CircularEaseOut instead of CircularEaseIn, which will both confuse callers and prevent using it without a cast; update the property type to CircularEaseIn to match the declaring class.
  • The ActionAnimation.RunAsync/RunFireAndForget logic uses MemberwiseClone while storing _cts/_tcs on the original instance; the clone used by AnimationService runs the action and completes its own _tcs, leaving the original _tcs awaited in RunAsync potentially never completing—consider moving the TCS/CTS creation to the clone or otherwise ensuring the instance awaited in RunAsync is the one whose completion is signaled from ComputeNextFrame.
  • In NScaleTransform the +/-/*// operators only operate on ScaleX/ScaleY and ignore CenterX/CenterY, which may lead to surprising behavior when animating centers; verify whether centers should also be interpolated/combined and adjust the operators (or document the current semantics) accordingly.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `CircularEaseIn` the `Shared` static property is typed as `CircularEaseOut` instead of `CircularEaseIn`, which will both confuse callers and prevent using it without a cast; update the property type to `CircularEaseIn` to match the declaring class.
- The `ActionAnimation.RunAsync`/`RunFireAndForget` logic uses `MemberwiseClone` while storing `_cts`/`_tcs` on the original instance; the clone used by `AnimationService` runs the action and completes its own `_tcs`, leaving the original `_tcs` awaited in `RunAsync` potentially never completing—consider moving the TCS/CTS creation to the clone or otherwise ensuring the instance awaited in `RunAsync` is the one whose completion is signaled from `ComputeNextFrame`.
- In `NScaleTransform` the `+`/`-`/`*`/`/` operators only operate on `ScaleX`/`ScaleY` and ignore `CenterX`/`CenterY`, which may lead to surprising behavior when animating centers; verify whether centers should also be interpolated/combined and adjust the operators (or document the current semantics) accordingly.

## Individual Comments

### Comment 1
<location path="PCL.Core/UI/Animation/Easings/CircularEaseIn.cs" line_range="7" />
<code_context>

 public class CircularEaseIn : Easing
 {
+    public static CircularEaseOut Shared { get; } = new();
+    
     protected override double EaseCore(double progress)
</code_context>
<issue_to_address>
**issue (bug_risk):** The `CircularEaseIn.Shared` singleton is declared with the wrong type.

`CircularEaseIn.Shared` is declared as `CircularEaseOut`, which appears to be a copy‑paste error and will cause incorrect typing/usage. It should be:

```csharp
public static CircularEaseIn Shared { get; } = new();
```
</issue_to_address>

### Comment 2
<location path="PCL.Core/UI/Animation/Core/FromToAnimationBase.cs" line_range="88-90" />
<code_context>

         // 如果 From 为空,则根据动画值类型设置初始值
-        From ??= ValueType == AnimationValueType.Relative ? default : _startValue;
+        if (!ValueProcessorManager.Equal(_startValue, From))
+        {
+           From = ValueType == AnimationValueType.Relative ? ValueProcessorManager.DefaultValue<T>() : _startValue;
+        }

</code_context>
<issue_to_address>
**issue (bug_risk):** The new `From` initialization logic overwrites explicit `From` values whenever they differ from the current target value.

Previously, `From` was only inferred when it was null/unspecified. With the new condition, any caller‑provided `From` that differs from `_startValue` is overwritten, so you can no longer animate from an arbitrary value.

If the goal is still “infer `From` only when it hasn’t been set”, consider instead checking for the unset/default case, e.g.:

```csharp
// if DefaultValue() represents "unset" for Relative animations
if (ValueProcessorManager.Equal(From, ValueProcessorManager.DefaultValue<T>()))
{
    From = ValueType == AnimationValueType.Relative
        ? ValueProcessorManager.DefaultValue<T>()
        : _startValue;
}
```

Alternatively, track an explicit `bool HasExplicitFrom` rather than inferring it via equality.
</issue_to_address>

### Comment 3
<location path="PCL.Core/UI/NScaleTransform.cs" line_range="79-81" />
<code_context>
+    
+    #region 运算符重载
+
+    public static NScaleTransform operator +(NScaleTransform a, NScaleTransform b) => new(a.ScaleX + b.ScaleX, a.ScaleY + b.ScaleY);
+    public static NScaleTransform operator -(NScaleTransform a, NScaleTransform b) => new(a.ScaleX - b.ScaleX, a.ScaleY - b.ScaleY);
+    public static NScaleTransform operator *(NScaleTransform a, float b) => new(a.ScaleX * b, a.ScaleY * b);
+
+    public static NScaleTransform operator /(NScaleTransform a, float b) =>
</code_context>
<issue_to_address>
**issue (bug_risk):** `NScaleTransform` arithmetic operators ignore `CenterX`/`CenterY`, which may lead to incorrect composite transforms.

The overloaded operators currently construct new instances with `CenterX`/`CenterY` implicitly set to 0, so center information is discarded during arithmetic and composition. If centers are meant to participate in interpolation/composition, the operators should also combine them, e.g.

```csharp
new(
    a.ScaleX + b.ScaleX,
    a.ScaleY + b.ScaleY,
    a.CenterX + b.CenterX,
    a.CenterY + b.CenterY)
```

`NRotateTransform`’s operators have the same issue: they modify only the angle and ignore the center.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +88 to +90
if (!ValueProcessorManager.Equal(_startValue, From))
{
From = ValueType == AnimationValueType.Relative ? ValueProcessorManager.DefaultValue<T>() : _startValue;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): 新的 From 初始化逻辑在 From 与当前目标值不同时会覆盖显式设置的 From 值。

之前,只有在 From 为 null/未指定时才会进行推断。使用新的条件后,任何与 _startValue 不相等的调用方提供的 From 都会被覆盖,因此就无法再从任意值开始做动画了。

如果目标仍然是“只有在尚未设置时才推断 From”,可以考虑改为检查未设置/默认的情况,例如:

// 如果对相对动画来说 DefaultValue() 表示“未设置”
if (ValueProcessorManager.Equal(From, ValueProcessorManager.DefaultValue<T>()))
{
    From = ValueType == AnimationValueType.Relative
        ? ValueProcessorManager.DefaultValue<T>()
        : _startValue;
}

或者,显式维护一个 bool HasExplicitFrom 来表示是否已显式设置,而不是通过相等性来推断。

Original comment in English

issue (bug_risk): The new From initialization logic overwrites explicit From values whenever they differ from the current target value.

Previously, From was only inferred when it was null/unspecified. With the new condition, any caller‑provided From that differs from _startValue is overwritten, so you can no longer animate from an arbitrary value.

If the goal is still “infer From only when it hasn’t been set”, consider instead checking for the unset/default case, e.g.:

// if DefaultValue() represents "unset" for Relative animations
if (ValueProcessorManager.Equal(From, ValueProcessorManager.DefaultValue<T>()))
{
    From = ValueType == AnimationValueType.Relative
        ? ValueProcessorManager.DefaultValue<T>()
        : _startValue;
}

Alternatively, track an explicit bool HasExplicitFrom rather than inferring it via equality.

Comment on lines +79 to +81
public static NScaleTransform operator +(NScaleTransform a, NScaleTransform b) => new(a.ScaleX + b.ScaleX, a.ScaleY + b.ScaleY);
public static NScaleTransform operator -(NScaleTransform a, NScaleTransform b) => new(a.ScaleX - b.ScaleX, a.ScaleY - b.ScaleY);
public static NScaleTransform operator *(NScaleTransform a, float b) => new(a.ScaleX * b, a.ScaleY * b);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): NScaleTransform 的算术运算符忽略了 CenterX/CenterY,这可能会导致组合变换结果不正确。

当前的运算符重载在构造新实例时,会隐式地将 CenterX/CenterY 设置为 0,从而在算术和组合过程中丢失中心信息。如果中心点应参与插值/组合,那么这些运算符也应同时组合中心值,例如:

new(
    a.ScaleX + b.ScaleX,
    a.ScaleY + b.ScaleY,
    a.CenterX + b.CenterX,
    a.CenterY + b.CenterY)

NRotateTransform 的运算符也存在同样的问题:它们只修改角度而忽略中心。

Original comment in English

issue (bug_risk): NScaleTransform arithmetic operators ignore CenterX/CenterY, which may lead to incorrect composite transforms.

The overloaded operators currently construct new instances with CenterX/CenterY implicitly set to 0, so center information is discarded during arithmetic and composition. If centers are meant to participate in interpolation/composition, the operators should also combine them, e.g.

new(
    a.ScaleX + b.ScaleX,
    a.ScaleY + b.ScaleY,
    a.CenterX + b.CenterX,
    a.CenterY + b.CenterY)

NRotateTransform’s operators have the same issue: they modify only the angle and ignore the center.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: XXL PR 大小评估:巨型 🛠️ 等待审查 Pull Request 已完善,等待维护者或负责人进行代码审查

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant