Jelajahi Sumber

Merge branch 'master' into feature/SkiaTypefaceFallback

Jumar Macato 6 tahun lalu
induk
melakukan
3f127d9da6
67 mengubah file dengan 1523 tambahan dan 572 penghapusan
  1. 28 1
      Avalonia.sln
  2. 20 1
      native/Avalonia.Native/src/OSX/window.mm
  3. 1 0
      nukebuild/Build.cs
  4. 33 2
      samples/RenderDemo/Pages/AnimationsPage.xaml
  5. 1 1
      samples/RenderDemo/Pages/ClippingPage.xaml
  6. 1 0
      src/Avalonia.Animation/Animatable.cs
  7. 170 29
      src/Avalonia.Animation/Animation.cs
  8. 103 101
      src/Avalonia.Animation/AnimationInstance`1.cs
  9. 28 45
      src/Avalonia.Animation/Animators/Animator`1.cs
  10. 21 0
      src/Avalonia.Animation/Animators/BoolAnimator.cs
  11. 24 0
      src/Avalonia.Animation/Animators/ByteAnimator.cs
  12. 17 0
      src/Avalonia.Animation/Animators/DecimalAnimator.cs
  13. 17 0
      src/Avalonia.Animation/Animators/DoubleAnimator.cs
  14. 17 0
      src/Avalonia.Animation/Animators/FloatAnimator.cs
  15. 24 0
      src/Avalonia.Animation/Animators/Int16Animator.cs
  16. 24 0
      src/Avalonia.Animation/Animators/Int32Animator.cs
  17. 24 0
      src/Avalonia.Animation/Animators/Int64Animator.cs
  18. 24 0
      src/Avalonia.Animation/Animators/UInt16Animator.cs
  19. 24 0
      src/Avalonia.Animation/Animators/UInt32Animator.cs
  20. 24 0
      src/Avalonia.Animation/Animators/UInt64Animator.cs
  21. 1 2
      src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs
  22. 0 35
      src/Avalonia.Animation/DoubleAnimator.cs
  23. 0 0
      src/Avalonia.Animation/Easing/IEasing.cs
  24. 176 0
      src/Avalonia.Animation/IterationCount.cs
  25. 2 2
      src/Avalonia.Animation/IterationCountTypeConverter.cs
  26. 7 8
      src/Avalonia.Animation/KeyFrame.cs
  27. 0 33
      src/Avalonia.Animation/KeyFramePair`1.cs
  28. 33 0
      src/Avalonia.Animation/KeyFrames.cs
  29. 2 1
      src/Avalonia.Animation/Properties/AssemblyInfo.cs
  30. 0 199
      src/Avalonia.Animation/RepeatCount.cs
  31. 5 2
      src/Avalonia.Animation/Transitions/DoubleTransition.cs
  32. 0 0
      src/Avalonia.Animation/Transitions/FloatTransition.cs
  33. 0 0
      src/Avalonia.Animation/Transitions/IntegerTransition.cs
  34. 0 1
      src/Avalonia.Controls/Panel.cs
  35. 0 2
      src/Avalonia.Native/PlatformThreadingInterface.cs
  36. 2 2
      src/Avalonia.Themes.Default/ProgressBar.xaml
  37. 70 0
      src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs
  38. 27 0
      src/Avalonia.Visuals/Animation/Animators/CornerRadiusAnimator.cs
  39. 17 0
      src/Avalonia.Visuals/Animation/Animators/PointAnimator.cs
  40. 23 0
      src/Avalonia.Visuals/Animation/Animators/RectAnimator.cs
  41. 17 0
      src/Avalonia.Visuals/Animation/Animators/SizeAnimator.cs
  42. 74 0
      src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs
  43. 17 0
      src/Avalonia.Visuals/Animation/Animators/ThicknessAnimator.cs
  44. 16 21
      src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
  45. 17 0
      src/Avalonia.Visuals/Animation/Animators/VectorAnimator.cs
  46. 27 17
      src/Avalonia.Visuals/Animation/CrossFade.cs
  47. 48 41
      src/Avalonia.Visuals/Animation/PageSlide.cs
  48. 36 0
      src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs
  49. 1 6
      src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs
  50. 25 0
      src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs
  51. 2 11
      src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs
  52. 25 0
      src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs
  53. 6 0
      src/Avalonia.Visuals/CornerRadius.cs
  54. 2 1
      src/Avalonia.Visuals/Media/Brush.cs
  55. 7 0
      src/Avalonia.Visuals/Media/Color.cs
  56. 3 0
      src/Avalonia.Visuals/Media/SolidColorBrush.cs
  57. 1 0
      src/Avalonia.Visuals/Media/Transform.cs
  58. 10 4
      src/Avalonia.Visuals/Point.cs
  59. 6 0
      src/Avalonia.Visuals/Rect.cs
  60. 6 0
      src/Avalonia.Visuals/Size.cs
  61. 38 1
      src/Avalonia.Visuals/Thickness.cs
  62. 8 2
      src/Avalonia.Visuals/Vector.cs
  63. 1 1
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  64. 77 0
      tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs
  65. 25 0
      tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj
  66. 10 0
      tests/Avalonia.Animation.UnitTests/Properties/AssemblyInfo.cs
  67. 28 0
      tests/Avalonia.Animation.UnitTests/TestClock.cs

+ 28 - 1
Avalonia.sln

@@ -1,4 +1,4 @@
-Microsoft Visual Studio Solution File, Format Version 12.00
+Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio 15
 VisualStudioVersion = 15.0.27130.2027
 MinimumVisualStudioVersion = 10.0.40219.1
@@ -193,6 +193,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Build.Tasks", "src
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build.csproj", "{3F00BC43-5095-477F-93D8-E65B08179A00}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}"
+EndProject
 Global
 	GlobalSection(SharedMSBuildProjectFiles) = preSolution
 		src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13
@@ -1720,6 +1722,30 @@ Global
 		{BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhone.Build.0 = Release|Any CPU
 		{BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
 		{BF28998D-072C-439A-AFBB-2FE5021241E0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|Any CPU.Build.0 = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.ActiveCfg = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhone.Build.0 = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhone.Build.0 = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.ActiveCfg = Release|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhone.Build.0 = Release|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+		{AF227847-E65C-4BE9-BCE9-B551357788E0}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -1773,6 +1799,7 @@ Global
 		{E1240B49-7B4B-4371-A00E-068778C5CF0B} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 		{D49233F8-F29C-47DD-9975-C4C9E4502720} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C}
 		{3C471044-3640-45E3-B1B2-16D2FF8399EE} = {E870DCD7-F46A-498D-83FC-D0FD13E0A11C}
+		{AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

+ 20 - 1
native/Avalonia.Native/src/OSX/window.mm

@@ -425,7 +425,7 @@ private:
                 [[Window parentWindow] removeChildWindow:Window];
             WindowBaseImpl::Show();
             
-            return SetWindowState(_lastWindowState);
+            return SetWindowState(Normal);
         }
     }
     
@@ -1184,6 +1184,25 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     }
 }
 
+- (void)windowDidMiniaturize:(NSNotification *)notification
+{
+    auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
+    
+    if(parent != nullptr)
+    {
+        parent->WindowStateChanged();
+    }
+}
+
+- (void)windowDidDeminiaturize:(NSNotification *)notification
+{
+    auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
+    
+    if(parent != nullptr)
+    {
+        parent->WindowStateChanged();
+    }
+}
 
 - (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame
 {

+ 1 - 0
nukebuild/Build.cs

@@ -124,6 +124,7 @@ partial class Build : NukeBuild
         .DependsOn(Compile)
         .Executes(() =>
         {
+            RunCoreTest("./tests/Avalonia.Animation.UnitTests", false);
             RunCoreTest("./tests/Avalonia.Base.UnitTests", false);
             RunCoreTest("./tests/Avalonia.Controls.UnitTests", false);
             RunCoreTest("./tests/Avalonia.Input.UnitTests", false);

+ 33 - 2
samples/RenderDemo/Pages/AnimationsPage.xaml

@@ -43,7 +43,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
       <Style Selector="Border.Rect1:pointerover">
         <Style.Animations>
           <Animation Duration="0:0:2.5"
-                     RepeatCount="4"
+                     IterationCount="4"
                      FillMode="None"
                      PlaybackDirection="AlternateReverse"
                      Easing="SineEaseInOut">
@@ -73,7 +73,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
         <Style.Animations>
           <Animation Duration="0:0:0.5"
                      Easing="QuadraticEaseInOut"
-                     RepeatCount="Loop">
+                     IterationCount="Infinite">
             <KeyFrame Cue="50%">
               <Setter Property="ScaleTransform.ScaleX" Value="0.8"/>
               <Setter Property="ScaleTransform.ScaleY" Value="0.8"/>
@@ -87,6 +87,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
           <Animation Duration="0:0:3" Easing="BounceEaseInOut">
             <KeyFrame Cue="48%">
               <Setter Property="TranslateTransform.Y" Value="-100"/>
+              <Setter Property="Background" Value="Magenta"/>
             </KeyFrame>
           </Animation>
         </Style.Animations>
@@ -103,6 +104,35 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
           </Animation>
         </Style.Animations>
       </Style>
+      <Style Selector="Border.Rect6">
+        <Style.Animations>
+          <Animation Duration="0:0:3"
+                     IterationCount="Infinite"
+                     PlaybackDirection="Alternate">
+            <KeyFrame Cue="0%">
+              <Setter Property="Background" Value="Red"/>
+            </KeyFrame>
+            <KeyFrame Cue="15%">
+              <Setter Property="Background" Value="Yellow"/>
+            </KeyFrame>
+            <KeyFrame Cue="30%">
+              <Setter Property="Background" Value="Green"/>
+            </KeyFrame>
+            <KeyFrame Cue="45%">
+              <Setter Property="Background" Value="Cyan"/>
+            </KeyFrame>
+            <KeyFrame Cue="60%">
+              <Setter Property="Background" Value="Blue"/>
+            </KeyFrame>
+            <KeyFrame Cue="75%">
+              <Setter Property="Background" Value="Indigo"/>
+            </KeyFrame>
+            <KeyFrame Cue="90%">
+              <Setter Property="Background" Value="Violet"/>
+            </KeyFrame>
+          </Animation>
+        </Style.Animations>
+      </Style>
     </Styles>
   </UserControl.Styles>
   <Grid>
@@ -120,6 +150,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
         <Border Classes="Test Rect3"/>
         <Border Classes="Test Rect4" Background="Navy"/>
         <Border Classes="Test Rect5" Background="SeaGreen"/>
+        <Border Classes="Test Rect6" Background="Red"/>
       </WrapPanel>
     </StackPanel>
   </Grid>

+ 1 - 1
samples/RenderDemo/Pages/ClippingPage.xaml

@@ -8,7 +8,7 @@ xmlns="https://github.com/avaloniaui">
         </Style>
         <Style Selector="Border#clipChild">
           <Style.Animations>
-            <Animation Duration="0:0:2" RepeatCount="Loop">
+            <Animation Duration="0:0:2" IterationCount="Infinite">
               <KeyFrame Cue="100%">
                 <Setter Property="RotateTransform.Angle" Value="360"/>
               </KeyFrame>

+ 1 - 0
src/Avalonia.Animation/Animatable.cs

@@ -7,6 +7,7 @@ using System.Linq;
 using System.Reactive.Linq;
 using Avalonia.Collections;
 using Avalonia.Data;
+using Avalonia.Animation.Animators; 
 
 namespace Avalonia.Animation
 {

+ 170 - 29
src/Avalonia.Animation/Animation.cs

@@ -7,68 +7,209 @@ using System.Linq;
 using System.Reactive.Disposables;
 using System.Reactive.Linq;
 using System.Threading.Tasks;
+using Avalonia.Animation.Animators;
 using Avalonia.Animation.Easings;
 using Avalonia.Collections;
+using Avalonia.Data;
+using Avalonia.Metadata;
 
 namespace Avalonia.Animation
 {
     /// <summary>
     /// Tracks the progress of an animation.
     /// </summary>
-    public class Animation : AvaloniaList<KeyFrame>, IAnimation
+    public class Animation : AvaloniaObject, IAnimation
     {
+        /// <summary>
+        /// Defines the <see cref="Duration"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Animation, TimeSpan> DurationProperty =
+            AvaloniaProperty.RegisterDirect<Animation, TimeSpan>(
+                nameof(_duration),
+                o => o._duration,
+                (o, v) => o._duration = v);
+
+        /// <summary>
+        /// Defines the <see cref="IterationCount"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Animation, IterationCount> IterationCountProperty =
+            AvaloniaProperty.RegisterDirect<Animation, IterationCount>(
+                nameof(_iterationCount),
+                o => o._iterationCount,
+                (o, v) => o._iterationCount = v);
+
+        /// <summary>
+        /// Defines the <see cref="PlaybackDirection"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Animation, PlaybackDirection> PlaybackDirectionProperty =
+            AvaloniaProperty.RegisterDirect<Animation, PlaybackDirection>(
+                nameof(_playbackDirection),
+                o => o._playbackDirection,
+                (o, v) => o._playbackDirection = v);
+
+        /// <summary>
+        /// Defines the <see cref="FillMode"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Animation, FillMode> FillModeProperty =
+            AvaloniaProperty.RegisterDirect<Animation, FillMode>(
+                nameof(_fillMode),
+                o => o._fillMode,
+                (o, v) => o._fillMode = v);
+
+        /// <summary>
+        /// Defines the <see cref="Easing"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Animation, Easing> EasingProperty =
+            AvaloniaProperty.RegisterDirect<Animation, Easing>(
+                nameof(_easing),
+                o => o._easing,
+                (o, v) => o._easing = v);
+
+        /// <summary>
+        /// Defines the <see cref="Delay"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Animation, TimeSpan> DelayProperty =
+            AvaloniaProperty.RegisterDirect<Animation, TimeSpan>(
+                nameof(_delay),
+                o => o._delay,
+                (o, v) => o._delay = v);
+
+        /// <summary>
+        /// Defines the <see cref="DelayBetweenIterations"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Animation, TimeSpan> DelayBetweenIterationsProperty =
+            AvaloniaProperty.RegisterDirect<Animation, TimeSpan>(
+                nameof(_delayBetweenIterations),
+                o => o._delayBetweenIterations,
+                (o, v) => o._delayBetweenIterations = v);
+
+        /// <summary>
+        /// Defines the <see cref="SpeedRatio"/> property.
+        /// </summary>
+        public static readonly DirectProperty<Animation, double> SpeedRatioProperty =
+            AvaloniaProperty.RegisterDirect<Animation, double>(
+                nameof(_speedRatio),
+                o => o._speedRatio,
+                (o, v) => o._speedRatio = v,
+                defaultBindingMode: BindingMode.TwoWay);
+
+        private TimeSpan _duration;
+        private IterationCount _iterationCount = new IterationCount(1);
+        private PlaybackDirection _playbackDirection;
+        private FillMode _fillMode;
+        private Easing _easing = new LinearEasing();
+        private TimeSpan _delay = TimeSpan.Zero;
+        private TimeSpan _delayBetweenIterations = TimeSpan.Zero;
+        private double _speedRatio = 1d;
+
         /// <summary>
         /// Gets or sets the active time of this animation.
         /// </summary>
-        public TimeSpan Duration { get; set; }
+        public TimeSpan Duration
+        {
+            get { return _duration; }
+            set { SetAndRaise(DurationProperty, ref _duration, value); }
+        }
 
         /// <summary>
         /// Gets or sets the repeat count for this animation.
         /// </summary>
-        public RepeatCount RepeatCount { get; set; }
+        public IterationCount IterationCount
+        {
+            get { return _iterationCount; }
+            set { SetAndRaise(IterationCountProperty, ref _iterationCount, value); }
+        }
 
         /// <summary>
         /// Gets or sets the playback direction for this animation.
         /// </summary>
-        public PlaybackDirection PlaybackDirection { get; set; }
+        public PlaybackDirection PlaybackDirection
+        {
+            get { return _playbackDirection; }
+            set { SetAndRaise(PlaybackDirectionProperty, ref _playbackDirection, value); }
+        }
 
         /// <summary>
         /// Gets or sets the value fill mode for this animation.
-        /// </summary>
-        public FillMode FillMode { get; set; }
+        /// </summary> 
+        public FillMode FillMode
+        {
+            get { return _fillMode; }
+            set { SetAndRaise(FillModeProperty, ref _fillMode, value); }
+        }
 
         /// <summary>
         /// Gets or sets the easing function to be used for this animation.
         /// </summary>
-        public Easing Easing { get; set; } = new LinearEasing();
-
-        /// <summary>
-        /// Gets or sets the speed multiple for this animation.
-        /// </summary>
-        public double SpeedRatio { get; set; } = 1d;
+        public Easing Easing
+        {
+            get { return _easing; }
+            set { SetAndRaise(EasingProperty, ref _easing, value); }
+        }
 
         /// <summary> 
-        /// Gets or sets the delay time for this animation. 
+        /// Gets or sets the initial delay time for this animation. 
         /// </summary> 
-        /// <remarks>
-        /// Describes a delay to be added before the animation starts, and optionally between 
-        /// repeats of the animation if <see cref="DelayBetweenIterations"/> is set. 
-        /// </remarks> 
-        public TimeSpan Delay { get; set; }
+        public TimeSpan Delay
+        {
+            get { return _delay; }
+            set { SetAndRaise(DelayProperty, ref _delay, value); }
+        }
 
         /// <summary> 
-        /// Gets or sets a value indicating whether <see cref="Delay"/> will be applied between 
-        /// iterations of the animation.
+        /// Gets or sets the delay time in between iterations.
+        /// </summary> 
+        public TimeSpan DelayBetweenIterations
+        {
+            get { return _delayBetweenIterations; }
+            set { SetAndRaise(DelayBetweenIterationsProperty, ref _delayBetweenIterations, value); }
+        }
+
+        /// <summary>
+        /// Gets or sets the speed multiple for this animation.
         /// </summary> 
-        /// <remarks>
-        /// If this property is not set, then <see cref="Delay"/> will only be applied to the first 
-        /// iteration of the animation. 
-        /// </remarks> 
-        public bool DelayBetweenIterations { get; set; }
+        public double SpeedRatio
+        {
+            get { return _speedRatio; }
+            set { SetAndRaise(SpeedRatioProperty, ref _speedRatio, value); }
+        }
+
+        /// <summary>
+        /// Obsolete: Do not use this property, use <see cref="IterationCount"/> instead.
+        /// </summary>
+        /// <value></value>
+        [Obsolete("This property has been superceded by IterationCount.")]
+        public string RepeatCount
+        {
+            get { return IterationCount.ToString(); }
+            set
+            {
+                var val = value.ToUpper();
+                val = val.Replace("LOOP", "INFINITE");
+                val = val.Replace("NONE", "1");
+                IterationCount = IterationCount.Parse(val);
+            }
+        }
+
+        /// <summary>
+        /// Gets the children of the <see cref="Animation"/>.
+        /// </summary>
+        [Content]
+        public KeyFrames Children { get; } = new KeyFrames();
 
         private readonly static List<(Func<AvaloniaProperty, bool> Condition, Type Animator)> Animators = new List<(Func<AvaloniaProperty, bool>, Type)>
         {
-            ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) )
+            ( prop => typeof(bool).IsAssignableFrom(prop.PropertyType), typeof(BoolAnimator) ),
+            ( prop => typeof(byte).IsAssignableFrom(prop.PropertyType), typeof(ByteAnimator) ),
+            ( prop => typeof(Int16).IsAssignableFrom(prop.PropertyType), typeof(Int16Animator) ),
+            ( prop => typeof(Int32).IsAssignableFrom(prop.PropertyType), typeof(Int32Animator) ),
+            ( prop => typeof(Int64).IsAssignableFrom(prop.PropertyType), typeof(Int64Animator) ),
+            ( prop => typeof(UInt16).IsAssignableFrom(prop.PropertyType), typeof(UInt16Animator) ),
+            ( prop => typeof(UInt32).IsAssignableFrom(prop.PropertyType), typeof(UInt32Animator) ),
+            ( prop => typeof(UInt64).IsAssignableFrom(prop.PropertyType), typeof(UInt64Animator) ),
+            ( prop => typeof(float).IsAssignableFrom(prop.PropertyType), typeof(FloatAnimator) ),
+            ( prop => typeof(double).IsAssignableFrom(prop.PropertyType), typeof(DoubleAnimator) ),
+            ( prop => typeof(decimal).IsAssignableFrom(prop.PropertyType), typeof(DecimalAnimator) ),
         };
 
         public static void RegisterAnimator<TAnimator>(Func<AvaloniaProperty, bool> condition)
@@ -95,9 +236,9 @@ namespace Avalonia.Animation
             var animatorKeyFrames = new List<AnimatorKeyFrame>();
             var subscriptions = new List<IDisposable>();
 
-            foreach (var keyframe in this)
+            foreach (var keyframe in Children)
             {
-                foreach (var setter in keyframe)
+                foreach (var setter in keyframe.Setters)
                 {
                     var handler = GetAnimatorType(setter.Property);
 
@@ -179,7 +320,7 @@ namespace Avalonia.Animation
         {
             var run = new TaskCompletionSource<object>();
 
-            if (this.RepeatCount == RepeatCount.Loop)
+            if (this.IterationCount == IterationCount.Infinite)
                 run.SetException(new InvalidOperationException("Looping animations must not use the Run method."));
 
             IDisposable subscriptions = null;

+ 103 - 101
src/Avalonia.Animation/AnimationInstance`1.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Linq;
 using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
 using Avalonia.Animation.Utils;
 using Avalonia.Data;
 using Avalonia.Reactive;
@@ -15,78 +16,77 @@ namespace Avalonia.Animation
     {
         private T _lastInterpValue;
         private T _firstKFValue;
-        private long _repeatCount;
-        private double _currentIteration;
-        private bool _isLooping;
+        private ulong? _iterationCount;
+        private ulong _currentIteration;
         private bool _gotFirstKFValue;
-        private bool _iterationDelay;
         private FillMode _fillMode;
-        private PlaybackDirection _animationDirection;
-        private Animator<T> _parent;
+        private PlaybackDirection _playbackDirection;
+        private Animator<T> _animator;
+        private Animation _animation;
         private Animatable _targetControl;
         private T _neutralValue;
-        private double _speedRatio;
-        private TimeSpan _delay;
+        private double _speedRatioConv;
+        private TimeSpan _initialDelay;
+        private TimeSpan _iterationDelay;
         private TimeSpan _duration;
         private Easings.Easing _easeFunc;
         private Action _onCompleteAction;
         private Func<double, T, T> _interpolator;
-        private IDisposable _timerSubscription;
+        private IDisposable _timerSub;
         private readonly IClock _baseClock;
         private IClock _clock;
 
         public AnimationInstance(Animation animation, Animatable control, Animator<T> animator, IClock baseClock, Action OnComplete, Func<double, T, T> Interpolator)
         {
-            if (animation.SpeedRatio <= 0)
-                throw new InvalidOperationException("Speed ratio cannot be negative or zero.");
+            _animator = animator;
+            _animation = animation;
+            _targetControl = control;
+            _onCompleteAction = OnComplete;
+            _interpolator = Interpolator;
+            _baseClock = baseClock;
+            _neutralValue = (T)_targetControl.GetValue(_animator.Property);
 
-            if (animation.Duration.TotalSeconds <= 0)
-                throw new InvalidOperationException("Duration cannot be negative or zero.");
+            FetchProperties();
+        }
 
-            _parent = animator;
-            _easeFunc = animation.Easing;
-            _targetControl = control;
-            _neutralValue = (T)_targetControl.GetValue(_parent.Property);
+        private void FetchProperties()
+        {
+            if (_animation.SpeedRatio < 0d)
+                throw new ArgumentOutOfRangeException("SpeedRatio value should not be negative.");
 
-            _speedRatio = animation.SpeedRatio;
+            if (_animation.Duration.TotalSeconds <= 0)
+                throw new InvalidOperationException("Duration value cannot be negative or zero.");
 
-            _delay = animation.Delay;
-            _duration = animation.Duration;
-            _iterationDelay = animation.DelayBetweenIterations;
+            _easeFunc = _animation.Easing;
 
-            switch (animation.RepeatCount.RepeatType)
-            {
-                case RepeatType.None:
-                    _repeatCount = 1;
-                    break;
-                case RepeatType.Loop:
-                    _isLooping = true;
-                    break;
-                case RepeatType.Repeat:
-                    _repeatCount = (long)animation.RepeatCount.Value;
-                    break;
-            }
+            _speedRatioConv = 1d / _animation.SpeedRatio;
 
-            _animationDirection = animation.PlaybackDirection;
-            _fillMode = animation.FillMode;
-            _onCompleteAction = OnComplete;
-            _interpolator = Interpolator;
-            _baseClock = baseClock;
-          }
+            _initialDelay = _animation.Delay;
+            _duration = _animation.Duration;
+            _iterationDelay = _animation.DelayBetweenIterations;
+
+            if (_animation.IterationCount.RepeatType == IterationType.Many)
+                _iterationCount = _animation.IterationCount.Value;
+            else
+                _iterationCount = null;
+
+            _playbackDirection = _animation.PlaybackDirection;
+            _fillMode = _animation.FillMode;
+        }
 
         protected override void Unsubscribed()
         {
-            //Animation may have been stopped before it has finished
+            // Animation may have been stopped before it has finished.
             ApplyFinalFill();
 
-            _timerSubscription?.Dispose();
+            _timerSub?.Dispose();
             _clock.PlayState = PlayState.Stop;
         }
 
         protected override void Subscribed()
         {
             _clock = new Clock(_baseClock);
-            _timerSubscription = _clock.Subscribe(Step);
+            _timerSub = _clock.Subscribe(Step);
         }
 
         public void Step(TimeSpan frameTick)
@@ -104,7 +104,7 @@ namespace Avalonia.Animation
         private void ApplyFinalFill()
         {
             if (_fillMode == FillMode.Forward || _fillMode == FillMode.Both)
-                _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue);
+                _targetControl.SetValue(_animator.Property, _lastInterpValue, BindingPriority.LocalValue);
         }
 
         private void DoComplete()
@@ -130,7 +130,7 @@ namespace Avalonia.Animation
 
             if (!_gotFirstKFValue)
             {
-                _firstKFValue = (T)_parent.First().Value;
+                _firstKFValue = (T)_animator.First().Value;
                 _gotFirstKFValue = true;
             }
         }
@@ -138,75 +138,77 @@ namespace Avalonia.Animation
         private void InternalStep(TimeSpan time)
         {
             DoPlayStates();
-            var delayEndpoint = _delay;
-            var iterationEndpoint = delayEndpoint + _duration;
-            var iterationTime = time;
 
-            //determine if time is currently in the first iteration.
-            if (time >= TimeSpan.Zero & time <= iterationEndpoint)
-            {
-                _currentIteration = 1;
-            }
-            else if (time > iterationEndpoint)
-            {
-                //Subtract first iteration to properly get the subsequent iteration time
-                iterationTime -= iterationEndpoint;
+            FetchProperties();
 
-                if (!_iterationDelay & delayEndpoint > TimeSpan.Zero)
-                {
-                    delayEndpoint = TimeSpan.Zero;
-                    iterationEndpoint = _duration;
-                }
-
-                //Calculate the current iteration number
-                _currentIteration = Math.Min(_repeatCount,(int)Math.Floor((double)((double)iterationTime.Ticks / iterationEndpoint.Ticks)) + 2);
-            }
-            else
-            {
-                return;
-            }
+            // Scale timebases according to speedratio.
+            var indexTime = time.Ticks;
+            var iterDuration = _duration.Ticks * _speedRatioConv;
+            var iterDelay = _iterationDelay.Ticks * _speedRatioConv;
+            var initDelay = _initialDelay.Ticks * _speedRatioConv;
 
-             // Determine if the current iteration should have its normalized time inverted.
-            bool isCurIterReverse = _animationDirection == PlaybackDirection.Normal ? false :
-                                    _animationDirection == PlaybackDirection.Alternate ? (_currentIteration % 2 == 0) ? false : true :
-                                    _animationDirection == PlaybackDirection.AlternateReverse ? (_currentIteration % 2 == 0) ? true : false :
-                                    _animationDirection == PlaybackDirection.Reverse ? true : false;
-   
-            if (!_isLooping)
-            {
-                var totalTime = _iterationDelay ? _repeatCount * ( _duration.Ticks + _delay.Ticks) : _repeatCount * _duration.Ticks + _delay.Ticks;
-                if (time.Ticks >= totalTime)
-                {
-                    var easedTime = _easeFunc.Ease(isCurIterReverse ? 0.0 : 1.0);
-                    _lastInterpValue = _interpolator(easedTime, _neutralValue);
-                   
-                    DoComplete();
-                    return;
-                }
-            }
-            iterationTime = TimeSpan.FromTicks((long)(iterationTime.Ticks % iterationEndpoint.Ticks));
-        
-            if (delayEndpoint > TimeSpan.Zero & iterationTime < delayEndpoint)
+            if (indexTime > 0 & indexTime <= initDelay)
             {
                 DoDelay();
             }
             else
             {
-                // Offset the delay time            
-                iterationTime -= delayEndpoint;
-                iterationEndpoint -= delayEndpoint;
+                // Calculate timebases.
+                var iterationTime = iterDuration + iterDelay;
+                var opsTime = indexTime - initDelay;
+                var playbackTime = opsTime % iterationTime;
 
-                // Normalize time
-                var interpVal = (double)iterationTime.Ticks / iterationEndpoint.Ticks;
+                _currentIteration = (ulong)(opsTime / iterationTime);
 
-                if (isCurIterReverse)
-                    interpVal = 1 - interpVal;
+                // Stop animation when the current iteration is beyond the iteration count.
+                if ((_currentIteration + 1) > _iterationCount)
+                    DoComplete();
 
-                // Ease and interpolate
-                var easedTime = _easeFunc.Ease(interpVal);
-                _lastInterpValue = _interpolator(easedTime, _neutralValue);
+                if (playbackTime <= iterDuration)
+                {
+                    // Normalize time for interpolation.
+                    var normalizedTime = playbackTime / iterDuration;
+
+                    // Check if normalized time needs to be reversed according to PlaybackDirection
+                    
+                    bool playbackReversed;
+                    switch (_playbackDirection)
+                    {
+                        case PlaybackDirection.Normal:
+                            playbackReversed = false;
+                            break;
+                        case PlaybackDirection.Reverse:
+                            playbackReversed = true;
+                            break;
+                        case PlaybackDirection.Alternate:
+                            playbackReversed = (_currentIteration % 2 == 0) ? false : true;
+                            break;
+                        case PlaybackDirection.AlternateReverse:
+                            playbackReversed = (_currentIteration % 2 == 0) ? true : false;
+                            break;
+                        default:
+                            throw new InvalidOperationException($"Animation direction value is unknown: {_playbackDirection}");
+                    }
+
+                    if (playbackReversed)
+                        normalizedTime = 1 - normalizedTime;
+
+                    // Ease and interpolate
+                    var easedTime = _easeFunc.Ease(normalizedTime);
+                    _lastInterpValue = _interpolator(easedTime, _neutralValue);
 
-                PublishNext(_lastInterpValue);
+                    PublishNext(_lastInterpValue);
+                }
+                else if (playbackTime > iterDuration &
+                         playbackTime <= iterationTime &
+                         iterDelay > 0)
+                {
+                    // The last iteration's trailing delay should be skipped.
+                    if ((_currentIteration + 1) < _iterationCount)
+                        DoDelay();
+                    else
+                        DoComplete();
+                }
             }
         }
     }

+ 28 - 45
src/Avalonia.Animation/Animator`1.cs → src/Avalonia.Animation/Animators/Animator`1.cs

@@ -10,10 +10,10 @@ using Avalonia.Collections;
 using Avalonia.Data;
 using Avalonia.Reactive;
 
-namespace Avalonia.Animation
+namespace Avalonia.Animation.Animators
 {
     /// <summary>
-    /// Base class for KeyFrames objects
+    /// Base class for <see cref="Animator{T}"/> objects
     /// </summary>
     public abstract class Animator<T> : AvaloniaList<AnimatorKeyFrame>, IAnimator
     {
@@ -45,17 +45,10 @@ namespace Avalonia.Animation
             return match.Subscribe(subject);
         }
 
-        /// <summary>
-        /// Get the nearest pair of cue-time ordered keyframes 
-        /// according to the given time parameter that is relative to the
-        /// total animation time and the normalized intra-keyframe pair time 
-        /// (i.e., the normalized time between the selected keyframes, relative to the
-        /// time parameter).
-        /// </summary>
-        /// <param name="animationTime">The time parameter, relative to the total animation time</param>
-        protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double animationTime)
+        protected T InterpolationHandler(double animationTime, T neutralValue)
         {
             AnimatorKeyFrame firstKeyframe, lastKeyframe;
+
             int kvCount = _convertedKeyframes.Count;
             if (kvCount > 2)
             {
@@ -84,38 +77,31 @@ namespace Avalonia.Animation
 
             double t0 = firstKeyframe.Cue.CueValue;
             double t1 = lastKeyframe.Cue.CueValue;
-            var intraframeTime = (animationTime - t0) / (t1 - t0);
-            var firstFrameData = (firstKeyframe.GetTypedValue<T>(), firstKeyframe.isNeutral);
-            var lastFrameData = (lastKeyframe.GetTypedValue<T>(), lastKeyframe.isNeutral);
-            return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
+
+            double progress = (animationTime - t0) / (t1 - t0);
+
+            T oldValue, newValue;
+
+            if (firstKeyframe.isNeutral)
+                oldValue = neutralValue;
+            else
+                oldValue = (T)firstKeyframe.Value;
+
+            if (lastKeyframe.isNeutral)
+                newValue = neutralValue;
+            else
+                newValue = (T)lastKeyframe.Value;
+
+            return Interpolate(progress, oldValue, newValue);
         }
 
         private int FindClosestBeforeKeyFrame(double time)
         {
-            int FindClosestBeforeKeyFrame(int startIndex, int length)
-            {
-                if (length == 0 || length == 1)
-                {
-                    return startIndex;
-                }
+            for (int i = 0; i < _convertedKeyframes.Count; i++)
+                if (_convertedKeyframes[i].Cue.CueValue > time)
+                    return i - 1;
 
-                int middle = startIndex + (length / 2);
-
-                if (_convertedKeyframes[middle].Cue.CueValue < time)
-                {
-                    return FindClosestBeforeKeyFrame(middle, length - middle);
-                }
-                else if (_convertedKeyframes[middle].Cue.CueValue > time)
-                {
-                    return FindClosestBeforeKeyFrame(startIndex, middle - startIndex);
-                }
-                else
-                {
-                    return middle;
-                }
-            }
-
-            return FindClosestBeforeKeyFrame(0, _convertedKeyframes.Count);
+            throw new Exception("Index time is out of keyframe time range.");
         }
 
         /// <summary>
@@ -129,18 +115,15 @@ namespace Avalonia.Animation
                 this,
                 clock ?? control.Clock ?? Clock.GlobalClock,
                 onComplete,
-                DoInterpolation);
+                InterpolationHandler);
             return control.Bind<T>((AvaloniaProperty<T>)Property, instance, BindingPriority.Animation);
         }
 
         /// <summary>
-        /// Interpolates a value given the desired time.
+        /// Interpolates in-between two key values given the desired progress time.
         /// </summary>
-        protected abstract T DoInterpolation(double time, T neutralValue);
+        public abstract T Interpolate(double progress, T oldValue, T newValue);
 
-        /// <summary>
-        /// Verifies, converts and sorts keyframe values according to this class's target type.
-        /// </summary>
         private void VerifyConvertKeyFrames()
         {
             foreach (AnimatorKeyFrame keyframe in this)
@@ -188,4 +171,4 @@ namespace Avalonia.Animation
             }
         }
     }
-}
+}

+ 21 - 0
src/Avalonia.Animation/Animators/BoolAnimator.cs

@@ -0,0 +1,21 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="bool"/> properties.
+    /// </summary>
+    public class BoolAnimator : Animator<bool>
+    {
+        /// <inheritdocs/>
+        public override bool Interpolate(double progress, bool oldValue, bool newValue)
+        {
+            if(progress >= 1d)
+                return newValue;
+            if(progress >= 0)
+                return oldValue;
+            return oldValue;
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Animation/Animators/ByteAnimator.cs

@@ -0,0 +1,24 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="byte"/> properties.
+    /// </summary>
+    public class ByteAnimator : Animator<byte>
+    {
+        const double maxVal = (double)byte.MaxValue;
+
+        /// <inheritdocs/>
+        public override byte Interpolate(double progress, byte oldValue, byte newValue)
+        {
+            var normOV = oldValue / maxVal;
+            var normNV = newValue / maxVal;
+            var deltaV = normNV - normOV;
+            return (byte)Math.Round(maxVal * ((deltaV * progress) + normOV));
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Animation/Animators/DecimalAnimator.cs

@@ -0,0 +1,17 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="decimal"/> properties.
+    /// </summary>
+    public class DecimalAnimator : Animator<decimal>
+    {
+        /// <inheritdocs/>
+        public override decimal Interpolate(double progress, decimal oldValue, decimal newValue)
+        {
+            return ((newValue - oldValue) * (decimal)progress) + oldValue;
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Animation/Animators/DoubleAnimator.cs

@@ -0,0 +1,17 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="double"/> properties.
+    /// </summary>
+    public class DoubleAnimator : Animator<double>
+    {
+        /// <inheritdocs/>
+        public override double Interpolate(double progress, double oldValue, double newValue)
+        {
+            return ((newValue - oldValue) * progress) + oldValue;
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Animation/Animators/FloatAnimator.cs

@@ -0,0 +1,17 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="float"/> properties.
+    /// </summary>
+    public class FloatAnimator : Animator<float>
+    {
+        /// <inheritdocs/>
+        public override float Interpolate(double progress, float oldValue, float newValue)
+        {
+            return (float)(((newValue - oldValue) * progress) + oldValue);
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Animation/Animators/Int16Animator.cs

@@ -0,0 +1,24 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="Int16"/> properties.
+    /// </summary>
+    public class Int16Animator : Animator<Int16>
+    {
+        const double maxVal = (double)Int16.MaxValue;
+
+        /// <inheritdocs/>
+        public override Int16 Interpolate(double progress, Int16 oldValue, Int16 newValue)
+        {
+            var normOV = oldValue / maxVal;
+            var normNV = newValue / maxVal;
+            var deltaV = normNV - normOV;
+            return (Int16)Math.Round(maxVal * ((deltaV * progress) + normOV));
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Animation/Animators/Int32Animator.cs

@@ -0,0 +1,24 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="Int32"/> properties.
+    /// </summary>
+    public class Int32Animator : Animator<Int32>
+    {
+        const double maxVal = (double)Int32.MaxValue;
+
+        /// <inheritdocs/>
+        public override Int32 Interpolate(double progress, Int32 oldValue, Int32 newValue)
+        {
+            var normOV = oldValue / maxVal;
+            var normNV = newValue / maxVal;
+            var deltaV = normNV - normOV;
+            return (Int32)Math.Round(maxVal * ((deltaV * progress) + normOV));
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Animation/Animators/Int64Animator.cs

@@ -0,0 +1,24 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="Int64"/> properties.
+    /// </summary>
+    public class Int64Animator : Animator<Int64>
+    {
+        const double maxVal = (double)Int64.MaxValue;
+
+        /// <inheritdocs/>
+        public override Int64 Interpolate(double progress, Int64 oldValue, Int64 newValue)
+        {
+            var normOV = oldValue / maxVal;
+            var normNV = newValue / maxVal;
+            var deltaV = normNV - normOV;
+            return (Int64)Math.Round(maxVal * ((deltaV * progress) + normOV));
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Animation/Animators/UInt16Animator.cs

@@ -0,0 +1,24 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="UInt16"/> properties.
+    /// </summary>
+    public class UInt16Animator : Animator<UInt16>
+    {
+        const double maxVal = (double)UInt16.MaxValue;
+
+        /// <inheritdocs/>
+        public override UInt16 Interpolate(double progress, UInt16 oldValue, UInt16 newValue)
+        {
+            var normOV = oldValue / maxVal;
+            var normNV = newValue / maxVal;
+            var deltaV = normNV - normOV;
+            return (UInt16)Math.Round(maxVal * ((deltaV * progress) + normOV));
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Animation/Animators/UInt32Animator.cs

@@ -0,0 +1,24 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="UInt32"/> properties.
+    /// </summary>
+    public class UInt32Animator : Animator<UInt32>
+    {
+        const double maxVal = (double)UInt32.MaxValue;
+
+        /// <inheritdocs/>
+        public override UInt32 Interpolate(double progress, UInt32 oldValue, UInt32 newValue)
+        {
+            var normOV = oldValue / maxVal;
+            var normNV = newValue / maxVal;
+            var deltaV = normNV - normOV;
+            return (UInt32)Math.Round(maxVal * ((deltaV * progress) + normOV));
+        }
+    }
+}

+ 24 - 0
src/Avalonia.Animation/Animators/UInt64Animator.cs

@@ -0,0 +1,24 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="UInt64"/> properties.
+    /// </summary>
+    public class UInt64Animator : Animator<UInt64>
+    {
+        const double maxVal = (double)UInt64.MaxValue;
+
+        /// <inheritdocs/>
+        public override UInt64 Interpolate(double progress, UInt64 oldValue, UInt64 newValue)
+        {
+            var normOV = oldValue / maxVal;
+            var normNV = newValue / maxVal;
+            var deltaV = normNV - normOV;
+            return (UInt64)Math.Round(maxVal * ((deltaV * progress) + normOV));
+        }
+    }
+}

+ 1 - 2
src/Avalonia.Animation/DisposeAnimationInstanceSubject.cs

@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.Linq;
 using System.Reactive.Linq;
+using Avalonia.Animation.Animators;
 using Avalonia.Animation.Utils;
 using Avalonia.Collections;
 using Avalonia.Data;
@@ -33,8 +34,6 @@ namespace Avalonia.Animation
             this._onComplete = onComplete;
             this._clock = clock;
         }
-
-
         public void Dispose()
         {
             _lastInstance?.Dispose();

+ 0 - 35
src/Avalonia.Animation/DoubleAnimator.cs

@@ -1,35 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-namespace Avalonia.Animation
-{
-    /// <summary>
-    /// Animator that handles <see cref="double"/> properties.
-    /// </summary>
-    public class DoubleAnimator : Animator<double>
-    {
-
-        /// <inheritdocs/>
-        protected override double DoInterpolation(double t, double neutralValue)
-        {
-            var pair = GetKFPairAndIntraKFTime(t);
-            double y0, y1;
-
-            var firstKF = pair.KFPair.FirstKeyFrame;
-            var secondKF = pair.KFPair.SecondKeyFrame;
-
-            if (firstKF.isNeutral)
-                y0 = neutralValue;
-            else
-                y0 = firstKF.TargetValue;
-
-            if (secondKF.isNeutral)
-                y1 = neutralValue;
-            else
-                y1 = secondKF.TargetValue;
-
-            // Do linear parametric interpolation 
-            return y0 + (pair.IntraKFTime) * (y1 - y0);
-        }
-    }
-}

+ 0 - 0
src/Avalonia.Animation/IEasing.cs → src/Avalonia.Animation/Easing/IEasing.cs


+ 176 - 0
src/Avalonia.Animation/IterationCount.cs

@@ -0,0 +1,176 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.ComponentModel;
+using System.Globalization;
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// Defines the valid modes for a <see cref="IterationCount"/>.
+    /// </summary>
+    public enum IterationType
+    {
+        Many,
+        Infinite
+    }
+
+    /// <summary>
+    /// Determines the number of iterations of an animation.
+    /// Also defines its repeat behavior. 
+    /// </summary>
+    [TypeConverter(typeof(IterationCountTypeConverter))]
+    public struct IterationCount : IEquatable<IterationCount>
+    {
+        private readonly IterationType _type;
+        private readonly ulong _value;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IterationCount"/> struct.
+        /// </summary>
+        /// <param name="value">The number of iterations of an animation.</param>
+        public IterationCount(ulong value)
+            : this(value, IterationType.Many)
+        {
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="IterationCount"/> struct.
+        /// </summary>
+        /// <param name="value">The size of the IterationCount.</param>
+        /// <param name="type">The unit of the IterationCount.</param>
+        public IterationCount(ulong value, IterationType type)
+        {
+            if (type > IterationType.Infinite)
+            {
+                throw new ArgumentException("Invalid value", "type");
+            }
+
+            _type = type;
+            _value = value;
+        }
+
+        /// <summary>
+        /// Gets an instance of <see cref="IterationCount"/> that indicates that an animation
+        /// should repeat forever.
+        /// </summary>
+        public static IterationCount Infinite => new IterationCount(0, IterationType.Infinite);
+
+        /// <summary>
+        /// Gets the unit of the <see cref="IterationCount"/>.
+        /// </summary>
+        public IterationType RepeatType => _type;
+
+        /// <summary>
+        /// Gets a value that indicates whether the <see cref="IterationCount"/> is set to loop.
+        /// </summary>
+        public bool IsInfinite => _type == IterationType.Infinite;
+
+        /// <summary>
+        /// Gets the number of repeat iterations.
+        /// </summary>
+        public ulong Value => _value;
+
+        /// <summary>
+        /// Compares two IterationCount structures for equality.
+        /// </summary>
+        /// <param name="a">The first IterationCount.</param>
+        /// <param name="b">The second IterationCount.</param>
+        /// <returns>True if the structures are equal, otherwise false.</returns>
+        public static bool operator ==(IterationCount a, IterationCount b)
+        {
+            return (a.IsInfinite && b.IsInfinite)
+                || (a._value == b._value && a._type == b._type);
+        }
+
+        /// <summary>
+        /// Compares two IterationCount structures for inequality.
+        /// </summary>
+        /// <param name="rc1">The first IterationCount.</param>
+        /// <param name="rc2">The first IterationCount.</param>
+        /// <returns>True if the structures are unequal, otherwise false.</returns>
+        public static bool operator !=(IterationCount rc1, IterationCount rc2)
+        {
+            return !(rc1 == rc2);
+        }
+
+        /// <summary>
+        /// Determines whether the <see cref="IterationCount"/> is equal to the specified object.
+        /// </summary>
+        /// <param name="o">The object with which to test equality.</param>
+        /// <returns>True if the objects are equal, otherwise false.</returns>
+        public override bool Equals(object o)
+        {
+            if (o == null)
+            {
+                return false;
+            }
+
+            if (!(o is IterationCount))
+            {
+                return false;
+            }
+
+            return this == (IterationCount)o;
+        }
+
+        /// <summary>
+        /// Compares two IterationCount structures for equality.
+        /// </summary>
+        /// <param name="IterationCount">The structure with which to test equality.</param>
+        /// <returns>True if the structures are equal, otherwise false.</returns>
+        public bool Equals(IterationCount IterationCount)
+        {
+            return this == IterationCount;
+        }
+
+        /// <summary>
+        /// Gets a hash code for the IterationCount.
+        /// </summary>
+        /// <returns>The hash code.</returns>
+        public override int GetHashCode()
+        {
+            return _value.GetHashCode() ^ _type.GetHashCode();
+        }
+
+        /// <summary>
+        /// Gets a string representation of the <see cref="IterationCount"/>.
+        /// </summary>
+        /// <returns>The string representation.</returns>
+        public override string ToString()
+        {
+            if (IsInfinite)
+            {
+                return "Infinite";
+            }
+
+            string s = _value.ToString();
+            return s;
+        }
+
+        /// <summary>
+        /// Parses a string to return a <see cref="IterationCount"/>.
+        /// </summary>
+        /// <param name="s">The string.</param>
+        /// <returns>The <see cref="IterationCount"/>.</returns>
+        public static IterationCount Parse(string s)
+        {
+            s = s.ToUpperInvariant().Trim();
+
+            if (s.EndsWith("INFINITE"))
+            {
+                return Infinite;
+            }
+            else
+            {
+                if (s.StartsWith("-"))
+                    throw new InvalidCastException("IterationCount can't be a negative number.");
+
+                var value = ulong.Parse(s, CultureInfo.InvariantCulture);
+
+                return new IterationCount(value);
+            }
+        }
+    }
+}

+ 2 - 2
src/Avalonia.Animation/RepeatCountTypeConverter.cs → src/Avalonia.Animation/IterationCountTypeConverter.cs

@@ -7,7 +7,7 @@ using System.Globalization;
 
 namespace Avalonia.Animation
 {
-    public class RepeatCountTypeConverter : TypeConverter
+    public class IterationCountTypeConverter : TypeConverter
     {
         public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
         {
@@ -16,7 +16,7 @@ namespace Avalonia.Animation
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
-            return RepeatCount.Parse((string)value);
+            return IterationCount.Parse((string)value);
         }
     }
 }

+ 7 - 8
src/Avalonia.Animation/KeyFrame.cs

@@ -4,6 +4,7 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Collections;
+using Avalonia.Metadata;
 
 namespace Avalonia.Animation
 {
@@ -17,7 +18,7 @@ namespace Avalonia.Animation
     /// Stores data regarding a specific key
     /// point and value in an animation.
     /// </summary>
-    public class KeyFrame : AvaloniaList<IAnimationSetter>
+    public class KeyFrame : AvaloniaObject
     {
         private TimeSpan _ktimeSpan;
         private Cue _kCue;
@@ -26,13 +27,11 @@ namespace Avalonia.Animation
         {
         }
 
-        public KeyFrame(IEnumerable<IAnimationSetter> items) : base(items)
-        {
-        }
-
-        public KeyFrame(params IAnimationSetter[] items) : base(items)
-        {
-        }
+        /// <summary>
+        /// Gets the setters of <see cref="KeyFrame"/>.
+        /// </summary>
+        [Content]
+        public AvaloniaList<IAnimationSetter> Setters { get; } = new AvaloniaList<IAnimationSetter>();
 
         internal KeyFrameTimingMode TimingMode { get; private set; }
 

+ 0 - 33
src/Avalonia.Animation/KeyFramePair`1.cs

@@ -1,33 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-namespace Avalonia.Animation
-{
-    /// <summary>
-    /// Represents a pair of keyframe, usually the
-    /// Start and End keyframes of a <see cref="Animator{T}"/> object.
-    /// </summary>
-    public struct KeyFramePair<T>
-    {
-        /// <summary>
-        /// Initializes this <see cref="KeyFramePair{T}"/>
-        /// </summary>
-        /// <param name="FirstKeyFrame"></param>
-        /// <param name="LastKeyFrame"></param>
-        public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this()
-        {
-            this.FirstKeyFrame = FirstKeyFrame;
-            this.SecondKeyFrame = LastKeyFrame;
-        }
-
-        /// <summary>
-        /// First <see cref="KeyFrame"/> object.
-        /// </summary>
-        public (T TargetValue, bool isNeutral) FirstKeyFrame { get; }
-
-        /// <summary>
-        /// Second <see cref="KeyFrame"/> object.
-        /// </summary>
-        public (T TargetValue, bool isNeutral) SecondKeyFrame { get; }
-    }
-}

+ 33 - 0
src/Avalonia.Animation/KeyFrames.cs

@@ -0,0 +1,33 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Collections.Generic;
+using Avalonia.Collections;
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// A collection of <see cref="KeyFrame"/>s.
+    /// </summary>
+    public class KeyFrames : AvaloniaList<KeyFrame>
+    {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="KeyFrames"/> class.
+        /// </summary>
+        public KeyFrames()
+        {
+            ResetBehavior = ResetBehavior.Remove;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="KeyFrames"/> class.
+        /// </summary>
+        /// <param name="items">The initial items in the collection.</param>
+        public KeyFrames(IEnumerable<KeyFrame> items)
+            : base(items)
+        {
+            ResetBehavior = ResetBehavior.Remove;
+        }
+    }
+}

+ 2 - 1
src/Avalonia.Animation/Properties/AssemblyInfo.cs

@@ -5,4 +5,5 @@ using Avalonia.Metadata;
 using System.Reflection;
 
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
-[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")]
+[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")]

+ 0 - 199
src/Avalonia.Animation/RepeatCount.cs

@@ -1,199 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.ComponentModel;
-using System.Globalization;
-
-namespace Avalonia.Animation
-{
-    /// <summary>
-    /// Defines the valid modes for a <see cref="RepeatCount"/>.
-    /// </summary>
-    public enum RepeatType
-    {
-        None,
-        Repeat,
-        Loop
-    }
-
-    /// <summary>
-    /// Determines the number of iterations of an animation.
-    /// Also defines its repeat behavior. 
-    /// </summary>
-    [TypeConverter(typeof(RepeatCountTypeConverter))]
-    public struct RepeatCount : IEquatable<RepeatCount>
-    {
-        private readonly RepeatType _type;
-        private readonly ulong _value;
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="RepeatCount"/> struct.
-        /// </summary>
-        /// <param name="value">The number of iterations of an animation.</param>
-        public RepeatCount(ulong value)
-            : this(value, RepeatType.Repeat)
-        {
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="RepeatCount"/> struct.
-        /// </summary>
-        /// <param name="value">The size of the RepeatCount.</param>
-        /// <param name="type">The unit of the RepeatCount.</param>
-        public RepeatCount(ulong value, RepeatType type)
-        {
-            if (type < RepeatType.None || type > RepeatType.Loop)
-            {
-                throw new ArgumentException("Invalid value", "type");
-            }
-
-            _type = type;
-            _value = value;
-        }
-
-        /// <summary>
-        /// Gets an instance of <see cref="RepeatCount"/> that indicates that an animation
-        /// should repeat forever.
-        /// </summary>
-        public static RepeatCount Loop => new RepeatCount(0, RepeatType.Loop);
-
-        /// <summary>
-        /// Gets an instance of <see cref="RepeatCount"/> that indicates that an animation
-        /// should not repeat.
-        /// </summary>
-        public static RepeatCount None => new RepeatCount(0, RepeatType.None);
-
-        /// <summary>
-        /// Gets the unit of the <see cref="RepeatCount"/>.
-        /// </summary>
-        public RepeatType RepeatType => _type;
-
-        /// <summary>
-        /// Gets a value that indicates whether the <see cref="RepeatCount"/> is set to loop.
-        /// </summary>
-        public bool IsLoop => _type == RepeatType.Loop;
-
-        /// <summary>
-        /// Gets a value that indicates whether the <see cref="RepeatCount"/> is set to not repeat.
-        /// </summary>
-        public bool IsNone => _type == RepeatType.None;
-
-        /// <summary>
-        /// Gets the number of repeat iterations.
-        /// </summary>
-        public ulong Value => _value;
-
-        /// <summary>
-        /// Compares two RepeatCount structures for equality.
-        /// </summary>
-        /// <param name="a">The first RepeatCount.</param>
-        /// <param name="b">The second RepeatCount.</param>
-        /// <returns>True if the structures are equal, otherwise false.</returns>
-        public static bool operator ==(RepeatCount a, RepeatCount b)
-        {
-            return (a.IsNone && b.IsNone) && (a.IsLoop && b.IsLoop)
-                || (a._value == b._value && a._type == b._type);
-        }
-
-        /// <summary>
-        /// Compares two RepeatCount structures for inequality.
-        /// </summary>
-        /// <param name="rc1">The first RepeatCount.</param>
-        /// <param name="rc2">The first RepeatCount.</param>
-        /// <returns>True if the structures are unequal, otherwise false.</returns>
-        public static bool operator !=(RepeatCount rc1, RepeatCount rc2)
-        {
-            return !(rc1 == rc2);
-        }
-
-        /// <summary>
-        /// Determines whether the <see cref="RepeatCount"/> is equal to the specified object.
-        /// </summary>
-        /// <param name="o">The object with which to test equality.</param>
-        /// <returns>True if the objects are equal, otherwise false.</returns>
-        public override bool Equals(object o)
-        {
-            if (o == null)
-            {
-                return false;
-            }
-
-            if (!(o is RepeatCount))
-            {
-                return false;
-            }
-
-            return this == (RepeatCount)o;
-        }
-
-        /// <summary>
-        /// Compares two RepeatCount structures for equality.
-        /// </summary>
-        /// <param name="RepeatCount">The structure with which to test equality.</param>
-        /// <returns>True if the structures are equal, otherwise false.</returns>
-        public bool Equals(RepeatCount RepeatCount)
-        {
-            return this == RepeatCount;
-        }
-
-        /// <summary>
-        /// Gets a hash code for the RepeatCount.
-        /// </summary>
-        /// <returns>The hash code.</returns>
-        public override int GetHashCode()
-        {
-            return _value.GetHashCode() ^ _type.GetHashCode();
-        }
-
-        /// <summary>
-        /// Gets a string representation of the <see cref="RepeatCount"/>.
-        /// </summary>
-        /// <returns>The string representation.</returns>
-        public override string ToString()
-        {
-            if (IsLoop)
-            {
-                return "Auto";
-            }
-            else if (IsNone)
-            {
-                return "None";
-            }
-
-            string s = _value.ToString();
-            return s;
-        }
-
-        /// <summary>
-        /// Parses a string to return a <see cref="RepeatCount"/>.
-        /// </summary>
-        /// <param name="s">The string.</param>
-        /// <returns>The <see cref="RepeatCount"/>.</returns>
-        public static RepeatCount Parse(string s)
-        {
-            s = s.ToUpperInvariant().Trim();
-
-            if (s == "NONE")
-            {
-                return None;
-            }
-            else if (s.EndsWith("LOOP"))
-            {
-                return Loop;
-            }
-            else
-            {
-                if(s.StartsWith("-"))
-                    throw new InvalidCastException("RepeatCount can't be a negative number.");
-
-                var value = ulong.Parse(s, CultureInfo.InvariantCulture);
-             
-                if (value == 1)
-                    return None;
-
-                return new RepeatCount(value);
-            }
-        }
-    }
-}

+ 5 - 2
src/Avalonia.Animation/DoubleTransition.cs → src/Avalonia.Animation/Transitions/DoubleTransition.cs

@@ -14,9 +14,12 @@ namespace Avalonia.Animation
         /// <inheritdocs/>
         public override IObservable<double> DoTransition(IObservable<double> progress, double oldValue, double newValue)
         {
-            var delta = newValue - oldValue;
             return progress
-                .Select(p => Easing.Ease(p) * delta + oldValue);
+                .Select(p =>
+                {
+                    var f = Easing.Ease(p);
+                    return ((newValue - oldValue) * f) + oldValue;
+                });
         }
     }
 }

+ 0 - 0
src/Avalonia.Animation/FloatTransition.cs → src/Avalonia.Animation/Transitions/FloatTransition.cs


+ 0 - 0
src/Avalonia.Animation/IntegerTransition.cs → src/Avalonia.Animation/Transitions/IntegerTransition.cs


+ 0 - 1
src/Avalonia.Controls/Panel.cs

@@ -31,7 +31,6 @@ namespace Avalonia.Controls
         static Panel()
         {
             AffectsRender<Panel>(BackgroundProperty);
-            ClipToBoundsProperty.OverrideDefaultValue<Panel>(true);
         }
 
         /// <summary>

+ 0 - 2
src/Avalonia.Native/PlatformThreadingInterface.cs

@@ -68,8 +68,6 @@ namespace Avalonia.Native
                     lock (l)
                     {
                         cancellation?.Cancel();
-                        cancellation?.Dispose();
-                        cancellation = null;
                     }
                 });
                 try

+ 2 - 2
src/Avalonia.Themes.Default/ProgressBar.xaml

@@ -33,7 +33,7 @@
   <Style Selector="ProgressBar:horizontal:indeterminate /template/ Border#PART_Indicator">
       <Style.Animations>
           <Animation Duration="0:0:3"
-                     RepeatCount="Loop"
+                     IterationCount="Infinite"
                      Easing="LinearEasing">
               <KeyFrame Cue="0%">
                   <Setter Property="TranslateTransform.X"
@@ -49,7 +49,7 @@
   <Style Selector="ProgressBar:vertical:indeterminate /template/ Border#PART_Indicator">
       <Style.Animations>
           <Animation Duration="0:0:3"
-                     RepeatCount="Loop"
+                     IterationCount="Infinite"
                      Easing="LinearEasing">
               <KeyFrame Cue="0%">
                   <Setter Property="TranslateTransform.Y"

+ 70 - 0
src/Avalonia.Visuals/Animation/Animators/ColorAnimator.cs

@@ -0,0 +1,70 @@
+// Original color interpolation code was written by Romain Guy and Francois Blavoet 
+// and adopted from LottieSharp Project (https://github.com/ascora/LottieSharp).
+
+using System;
+using System.Reactive.Disposables;
+using Avalonia.Logging;
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that interpolates <see cref="Color"/> through 
+    /// gamma sRGB color space for better visual result.
+    /// </summary>
+    public class ColorAnimator : Animator<Color>
+    {
+        // Opto-electronic conversion function for the sRGB color space
+        // Takes a gamma-encoded sRGB value and converts it to a linear sRGB value
+        private static double OECF_sRGB(double linear)
+        {
+            // IEC 61966-2-1:1999
+            return linear <= 0.0031308d ? linear * 12.92d : (double)(Math.Pow(linear, 1.0d / 2.4d) * 1.055d - 0.055d);
+        }
+
+        // Electro-optical conversion function for the sRGB color space
+        // Takes a linear sRGB value and converts it to a gamma-encoded sRGB value
+        private static double EOCF_sRGB(double srgb)
+        {
+            // IEC 61966-2-1:1999
+            return srgb <= 0.04045d ? srgb / 12.92d : (double)Math.Pow((srgb + 0.055d) / 1.055d, 2.4d);
+        }
+
+        public override Color Interpolate(double progress, Color oldValue, Color newValue)
+        {
+            // normalize sRGB values.
+            var oldA = oldValue.A / 255d;
+            var oldR = oldValue.R / 255d;
+            var oldG = oldValue.G / 255d;
+            var oldB = oldValue.B / 255d;
+
+            var newA = newValue.A / 255d;
+            var newR = newValue.R / 255d;
+            var newG = newValue.G / 255d;
+            var newB = newValue.B / 255d;
+
+            // convert from sRGB to linear
+            oldR = EOCF_sRGB(oldR);
+            oldG = EOCF_sRGB(oldG);
+            oldB = EOCF_sRGB(oldB);
+
+            newR = EOCF_sRGB(newR);
+            newG = EOCF_sRGB(newG);
+            newB = EOCF_sRGB(newB);
+
+            // compute the interpolated color in linear space
+            var a = oldA + progress * (newA - oldA);
+            var r = oldR + progress * (newR - oldR);
+            var g = oldG + progress * (newG - oldG);
+            var b = oldB + progress * (newB - oldB);
+
+            // convert back to sRGB in the [0..255] range
+            a = a * 255d;
+            r = OECF_sRGB(r) * 255d;
+            g = OECF_sRGB(g) * 255d;
+            b = OECF_sRGB(b) * 255d;
+
+            return new Color((byte)Math.Round(a), (byte)Math.Round(r), (byte)Math.Round(g), (byte)Math.Round(b));
+        }
+    }
+}

+ 27 - 0
src/Avalonia.Visuals/Animation/Animators/CornerRadiusAnimator.cs

@@ -0,0 +1,27 @@
+using System;
+using Avalonia.Logging;
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="CornerRadius"/> properties.
+    /// </summary>
+    public class CornerRadiusAnimator : Animator<CornerRadius>
+    {
+        public override CornerRadius Interpolate(double progress, CornerRadius oldValue, CornerRadius newValue)
+        {
+            var deltaTL = newValue.TopLeft - oldValue.TopLeft;
+            var deltaTR = newValue.TopRight - oldValue.TopRight;
+            var deltaBR = newValue.BottomRight - oldValue.BottomRight;
+            var deltaBL = newValue.BottomLeft - oldValue.BottomLeft;
+
+            var nTL = progress * deltaTL + oldValue.TopLeft;
+            var nTR = progress * deltaTR + oldValue.TopRight;
+            var nBR = progress * deltaBR + oldValue.BottomRight;
+            var nBL = progress * deltaBL + oldValue.BottomLeft;
+
+            return new CornerRadius(nTL, nTR, nBR, nBL);
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Visuals/Animation/Animators/PointAnimator.cs

@@ -0,0 +1,17 @@
+using System;
+using Avalonia.Logging;
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="Point"/> properties.
+    /// </summary>
+    public class PointAnimator : Animator<Point>
+    {
+        public override Point Interpolate(double progress, Point oldValue, Point newValue)
+        { 
+            return ((newValue - oldValue) * progress) + oldValue;
+        }
+    }
+}

+ 23 - 0
src/Avalonia.Visuals/Animation/Animators/RectAnimator.cs

@@ -0,0 +1,23 @@
+using System;
+using Avalonia.Logging;
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="Rect"/> properties.
+    /// </summary>
+    public class RectAnimator : Animator<Rect>
+    {
+        public override Rect Interpolate(double progress, Rect oldValue, Rect newValue)
+        {
+            var deltaPos = newValue.Position - oldValue.Position;
+            var deltaSize = newValue.Size - oldValue.Size;
+
+            var newPos = (deltaPos * progress) + oldValue.Position;
+            var newSize = (deltaSize * progress) + oldValue.Size;
+
+            return new Rect(newPos, newSize);
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Visuals/Animation/Animators/SizeAnimator.cs

@@ -0,0 +1,17 @@
+using System;
+using Avalonia.Logging;
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="Size"/> properties.
+    /// </summary>
+    public class SizeAnimator : Animator<Size>
+    {
+        public override Size Interpolate(double progress, Size oldValue, Size newValue)
+        {
+            return ((newValue - oldValue) * progress) + oldValue;
+        }
+    }
+}

+ 74 - 0
src/Avalonia.Visuals/Animation/Animators/SolidColorBrushAnimator.cs

@@ -0,0 +1,74 @@
+using System;
+using System.Reactive.Disposables;
+using Avalonia.Logging;
+using Avalonia.Media;
+using Avalonia.Media.Immutable;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="SolidColorBrush"/>. 
+    /// </summary>
+    public class SolidColorBrushAnimator : Animator<SolidColorBrush>
+    {
+        ColorAnimator _colorAnimator;
+
+        void InitializeColorAnimator()
+        {
+            _colorAnimator = new ColorAnimator();
+
+            foreach (AnimatorKeyFrame keyframe in this)
+            {
+                _colorAnimator.Add(keyframe);
+            }
+
+            _colorAnimator.Property = SolidColorBrush.ColorProperty;
+        }
+
+        public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> match, Action onComplete)
+        {
+            foreach (var keyframe in this)
+            {
+                if (keyframe.Value as ISolidColorBrush == null)
+                    return Disposable.Empty;
+
+                // Preprocess keyframe values to Color if the xaml parser converts them to ISCB.
+                if (keyframe.Value.GetType() == typeof(ImmutableSolidColorBrush))
+                {
+                    keyframe.Value = ((ImmutableSolidColorBrush)keyframe.Value).Color;
+                }
+            }
+
+            // Add SCB if the target prop is empty.
+            if (control.GetValue(Property) == null)
+                control.SetValue(Property, new SolidColorBrush(Colors.Transparent));
+
+            var targetVal = control.GetValue(Property);
+
+            // Continue if target prop is not empty & is a SolidColorBrush derivative. 
+            if (typeof(ISolidColorBrush).IsAssignableFrom(targetVal.GetType()))
+            {
+                if (_colorAnimator == null)
+                    InitializeColorAnimator();
+
+                SolidColorBrush finalTarget;
+
+                // If it's ISCB, change it back to SCB.
+                if (targetVal.GetType() == typeof(ImmutableSolidColorBrush))
+                {
+                    var col = (ImmutableSolidColorBrush)targetVal;
+                    targetVal = new SolidColorBrush(col.Color);
+                    control.SetValue(Property, targetVal);
+                }
+
+                finalTarget = targetVal as SolidColorBrush;
+
+                return _colorAnimator.Apply(animation, finalTarget, clock ?? control.Clock, match, onComplete);
+            }
+
+            return Disposable.Empty;
+        }
+
+        public override SolidColorBrush Interpolate(double p, SolidColorBrush o, SolidColorBrush n) => null;
+    }
+}

+ 17 - 0
src/Avalonia.Visuals/Animation/Animators/ThicknessAnimator.cs

@@ -0,0 +1,17 @@
+using System;
+using Avalonia.Logging;
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="Thickness"/> properties.
+    /// </summary>
+    public class ThicknessAnimator : Animator<Thickness>
+    {
+        public override Thickness Interpolate(double progress, Thickness oldValue, Thickness newValue)
+        {
+            return ((newValue - oldValue) * progress) + oldValue;
+        }
+    }
+}

+ 16 - 21
src/Avalonia.Visuals/Animation/TransformAnimator.cs → src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs

@@ -2,14 +2,14 @@
 using Avalonia.Logging;
 using Avalonia.Media;
 
-namespace Avalonia.Animation
+namespace Avalonia.Animation.Animators
 {
     /// <summary>
     /// Animator that handles <see cref="Transform"/> properties.
     /// </summary>
     public class TransformAnimator : Animator<double>
     {
-        DoubleAnimator childAnimator;
+        DoubleAnimator _doubleAnimator;
 
         /// <inheritdoc/>
         public override IDisposable Apply(Animation animation, Animatable control, IClock clock, IObservable<bool> obsMatch, Action onComplete)
@@ -27,7 +27,7 @@ namespace Avalonia.Animation
                     // default RenderTransform order.
 
                     normalTransform.Children.Add(new ScaleTransform());
-                    normalTransform.Children.Add(new SkewTransform()); 
+                    normalTransform.Children.Add(new SkewTransform());
                     normalTransform.Children.Add(new RotateTransform());
                     normalTransform.Children.Add(new TranslateTransform());
 
@@ -36,15 +36,22 @@ namespace Avalonia.Animation
 
                 var renderTransformType = ctrl.RenderTransform.GetType();
 
-                if (childAnimator == null)
+                if (_doubleAnimator == null)
                 {
-                    InitializeChildAnimator();
+                    _doubleAnimator = new DoubleAnimator();
+
+                    foreach (AnimatorKeyFrame keyframe in this)
+                    {
+                        _doubleAnimator.Add(keyframe);
+                    }
+
+                    _doubleAnimator.Property = Property;
                 }
 
                 // It's a transform object so let's target that.
                 if (renderTransformType == Property.OwnerType)
                 {
-                    return childAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
+                    return _doubleAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
                 }
                 // It's a TransformGroup and try finding the target there.
                 else if (renderTransformType == typeof(TransformGroup))
@@ -53,7 +60,7 @@ namespace Avalonia.Animation
                     {
                         if (transform.GetType() == Property.OwnerType)
                         {
-                            return childAnimator.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete);
+                            return _doubleAnimator.Apply(animation, transform, clock ?? control.Clock, obsMatch, onComplete);
                         }
                     }
                 }
@@ -73,19 +80,7 @@ namespace Avalonia.Animation
             return null;
         }
 
-        void InitializeChildAnimator()
-        {
-            childAnimator = new DoubleAnimator();
-
-            foreach (AnimatorKeyFrame keyframe in this)
-            {
-                childAnimator.Add(keyframe);
-            }
-
-            childAnimator.Property = Property;
-        }
-
-        /// <inheritdocs/>
-        protected override double DoInterpolation(double time, double neutralValue) => 0;
+        /// <inheritdocs/> 
+        public override double Interpolate(double p, double o, double n) => 0;
     }
 }

+ 17 - 0
src/Avalonia.Visuals/Animation/Animators/VectorAnimator.cs

@@ -0,0 +1,17 @@
+using System;
+using Avalonia.Logging;
+using Avalonia.Media;
+
+namespace Avalonia.Animation.Animators
+{
+    /// <summary>
+    /// Animator that handles <see cref="Vector"/> properties.
+    /// </summary>
+    public class VectorAnimator : Animator<Vector>
+    {
+        public override Vector Interpolate(double progress, Vector oldValue, Vector newValue)
+        {
+            return ((newValue - oldValue) * progress) + oldValue;
+        }
+    }
+}

+ 27 - 17
src/Avalonia.Visuals/Animation/CrossFade.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Animation
         /// Initializes a new instance of the <see cref="CrossFade"/> class.
         /// </summary>
         public CrossFade()
-            :this(TimeSpan.Zero)
+            : this(TimeSpan.Zero)
         {
         }
 
@@ -33,30 +33,40 @@ namespace Avalonia.Animation
         {
             _fadeOutAnimation = new Animation
             {
-                new KeyFrame
-                (
-                    new Setter
+                Children =
+                {
+                    new KeyFrame()
                     {
-                        Property = Visual.OpacityProperty,
-                        Value = 0d
+                        Setters =
+                        {
+                            new Setter
+                            {
+                                Property = Visual.OpacityProperty,
+                                Value = 0d
+                            }
+                        },
+                        Cue = new Cue(1d)
                     }
-                )
-                {
-                    Cue = new Cue(1d)
+
                 }
             };
             _fadeInAnimation = new Animation
             {
-                new KeyFrame
-                (
-                    new Setter
+                Children =
+                {
+                    new KeyFrame()
                     {
-                        Property = Visual.OpacityProperty,
-                        Value = 0d
+                        Setters =
+                        {
+                            new Setter
+                            {
+                                Property = Visual.OpacityProperty,
+                                Value = 0d
+                            }
+                        },
+                        Cue = new Cue(0d)
                     }
-                )
-                {
-                    Cue = new Cue(0d)
+
                 }
             };
             _fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration;

+ 48 - 41
src/Avalonia.Visuals/Animation/PageSlide.cs

@@ -74,34 +74,36 @@ namespace Avalonia.Animation
             var distance = Orientation == SlideAxis.Horizontal ? parent.Bounds.Width : parent.Bounds.Height;
             var translateProperty = Orientation == SlideAxis.Horizontal ? TranslateTransform.XProperty : TranslateTransform.YProperty;
 
-
-            // TODO: Implement relevant transition logic here (or discard this class)
-            // in favor of XAML based transition for pages
             if (from != null)
             {
                 var animation = new Animation
                 {
-                    new KeyFrame
-                    (
-                        new Setter
-                        {
-                            Property = translateProperty,
-                            Value = 0d
-                        }
-                    )
+                    Children = 
                     {
-                        Cue = new Cue(0d)
-                    },
-                    new KeyFrame
-                    (
-                        new Setter
+                        new KeyFrame
                         {
-                            Property = translateProperty,
-                            Value = forward ? -distance : distance
-                        }
-                    )
-                    {
-                        Cue = new Cue(1d)
+                            Setters =
+                            {
+                                new Setter
+                                {
+                                    Property = translateProperty,
+                                Value = 0d
+                                }
+                            },
+                            Cue = new Cue(0d)
+                        },
+                        new KeyFrame
+                        {
+                            Setters =
+                            {
+                                new Setter
+                                {
+                                    Property = translateProperty,
+                                    Value = forward ? -distance : distance
+                                }
+                            },
+                            Cue = new Cue(1d)
+                        }                       
                     }
                 };
                 animation.Duration = Duration;
@@ -113,29 +115,34 @@ namespace Avalonia.Animation
                 to.IsVisible = true;
                 var animation = new Animation
                 {
+                    Children =
+                    {
 
-                    new KeyFrame
-                    (
-                        new Setter
+                        new KeyFrame
                         {
-                            Property = translateProperty,
-                            Value = forward ? distance : -distance
-                        }
-                    )
-                    {
-                        Cue = new Cue(0d)
-                    },
-                    new KeyFrame
-                    (
-                        new Setter
+                            Setters =
+                            {
+                                new Setter
+                                {
+                                    Property = translateProperty,
+                                    Value = forward ? distance : -distance
+                                }
+                            },
+                            Cue = new Cue(0d)
+                        },
+                        new KeyFrame
                         {
-                            Property = translateProperty,
-                            Value = 0d
+                            Setters =
+                            {
+                                new Setter
+                                {
+                                    Property = translateProperty,
+                                    Value = 0d
+                                }
+                            },
+                            Cue = new Cue(1d)
                         }
-                    )
-                    {
-                        Cue = new Cue(1d)
-                    },
+                    }
                 };
                 animation.Duration = Duration;
                 tasks.Add(animation.RunAsync(to));

+ 36 - 0
src/Avalonia.Visuals/Animation/Transitions/CornerRadiusTransition.cs

@@ -0,0 +1,36 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Linq;
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="CornerRadius"/> type.
+    /// </summary>  
+    public class CornerRadiusTransition : Transition<CornerRadius>
+    {
+        /// <inheritdocs/>
+        public override IObservable<CornerRadius> DoTransition(IObservable<double> progress, CornerRadius oldValue, CornerRadius newValue)
+        {
+            return progress
+                .Select(p =>
+                {
+                    var f = Easing.Ease(p);
+
+                    var deltaTL = newValue.TopLeft - oldValue.TopLeft;
+                    var deltaTR = newValue.TopRight - oldValue.TopRight;
+                    var deltaBR = newValue.BottomRight - oldValue.BottomRight;
+                    var deltaBL = newValue.BottomLeft - oldValue.BottomLeft;
+
+                    var nTL = f * deltaTL + oldValue.TopLeft;
+                    var nTR = f * deltaTR + oldValue.TopRight;
+                    var nBR = f * deltaBR + oldValue.BottomRight;
+                    var nBL = f * deltaBL + oldValue.BottomLeft;
+
+                    return new CornerRadius(nTL, nTR, nBR, nBL);
+                });
+        }
+    }
+}

+ 1 - 6
src/Avalonia.Visuals/Animation/PointTransition.cs → src/Avalonia.Visuals/Animation/Transitions/PointTransition.cs

@@ -14,16 +14,11 @@ namespace Avalonia.Animation
         /// <inheritdocs/>
         public override IObservable<Point> DoTransition(IObservable<double> progress, Point oldValue, Point newValue)
         {
-            var deltaX = newValue.X - oldValue.Y;
-            var deltaY = newValue.X - oldValue.Y;
-
             return progress
                 .Select(p =>
                 {
                     var f = Easing.Ease(p);
-                    var nX = f * deltaX + oldValue.X;
-                    var nY = f * deltaY + oldValue.Y;
-                    return new Point(nX, nY);
+                    return ((newValue - oldValue) * f) + oldValue;
                 });
         }
     }

+ 25 - 0
src/Avalonia.Visuals/Animation/Transitions/SizeTransition.cs

@@ -0,0 +1,25 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Linq;
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Size"/> type.
+    /// </summary>  
+    public class SizeTransition : Transition<Size>
+    {
+        /// <inheritdocs/>
+        public override IObservable<Size> DoTransition(IObservable<double> progress, Size oldValue, Size newValue)
+        {
+            return progress
+                .Select(p =>
+                {
+                    var f = Easing.Ease(p);
+                    return ((newValue - oldValue) * f) + oldValue;
+                });
+        }
+    }
+}

+ 2 - 11
src/Avalonia.Visuals/Animation/ThicknessTransition.cs → src/Avalonia.Visuals/Animation/Transitions/ThicknessTransition.cs

@@ -14,20 +14,11 @@ namespace Avalonia.Animation
         /// <inheritdocs/>
         public override IObservable<Thickness> DoTransition(IObservable<double> progress, Thickness oldValue, Thickness newValue)
         {
-            var deltaL = newValue.Left - oldValue.Left;
-            var deltaT = newValue.Top - oldValue.Top;
-            var deltaR = newValue.Right - oldValue.Right;
-            var deltaB = newValue.Bottom - oldValue.Bottom;
-
             return progress
-                .Select(p => 
+                .Select(p =>
                 {
                     var f = Easing.Ease(p);
-                    var nL = f * deltaL + oldValue.Left;
-                    var nT = f * deltaT + oldValue.Right;
-                    var nR = f * deltaR + oldValue.Top;
-                    var nB = f * deltaB + oldValue.Bottom;
-                    return new Thickness(nL, nT, nR, nB);
+                    return ((newValue - oldValue) * f) + oldValue;
                 });
         }
     }

+ 25 - 0
src/Avalonia.Visuals/Animation/Transitions/VectorTransition.cs

@@ -0,0 +1,25 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System;
+using System.Reactive.Linq;
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// Transition class that handles <see cref="AvaloniaProperty"/> with <see cref="Vector"/> type.
+    /// </summary>  
+    public class VectorTransition : Transition<Vector>
+    {
+        /// <inheritdocs/>
+        public override IObservable<Vector> DoTransition(IObservable<double> progress, Vector oldValue, Vector newValue)
+        {
+            return progress
+                .Select(p =>
+                {
+                    var f = Easing.Ease(p);
+                    return ((newValue - oldValue) * f) + oldValue;
+                });
+        }
+    }
+}

+ 6 - 0
src/Avalonia.Visuals/CornerRadius.cs

@@ -3,12 +3,18 @@
 
 using System;
 using System.Globalization;
+using Avalonia.Animation.Animators;
 using Avalonia.Utilities;
 
 namespace Avalonia
 {
     public struct CornerRadius
     {
+        static CornerRadius()
+        {
+            Animation.Animation.RegisterAnimator<CornerRadiusAnimator>(prop => typeof(CornerRadius).IsAssignableFrom(prop.PropertyType));
+        }
+
         public CornerRadius(double uniformRadius)
         {
             TopLeft = TopRight = BottomLeft = BottomRight = uniformRadius;

+ 2 - 1
src/Avalonia.Visuals/Media/Brush.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.ComponentModel;
+using Avalonia.Animation;
 
 namespace Avalonia.Media
 {
@@ -10,7 +11,7 @@ namespace Avalonia.Media
     /// Describes how an area is painted.
     /// </summary>
     [TypeConverter(typeof(BrushConverter))]
-    public abstract class Brush : AvaloniaObject, IMutableBrush
+    public abstract class Brush : Animatable, IMutableBrush
     {
         /// <summary>
         /// Defines the <see cref="Opacity"/> property.

+ 7 - 0
src/Avalonia.Visuals/Media/Color.cs

@@ -3,6 +3,8 @@
 
 using System;
 using System.Globalization;
+using Avalonia.Animation;
+using Avalonia.Animation.Animators;
 
 namespace Avalonia.Media
 {
@@ -11,6 +13,11 @@ namespace Avalonia.Media
     /// </summary>
     public readonly struct Color
     {
+        static Color()
+        {
+            Animation.Animation.RegisterAnimator<ColorAnimator>(prop => typeof(Color).IsAssignableFrom(prop.PropertyType));
+        }
+
         /// <summary>
         /// Gets or sets the Alpha component of the color.
         /// </summary>

+ 3 - 0
src/Avalonia.Visuals/Media/SolidColorBrush.cs

@@ -1,6 +1,8 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Animation;
+using Avalonia.Animation.Animators;
 using Avalonia.Media.Immutable;
 
 namespace Avalonia.Media
@@ -18,6 +20,7 @@ namespace Avalonia.Media
 
         static SolidColorBrush()
         {
+            Animation.Animation.RegisterAnimator<SolidColorBrushAnimator>(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType));
             AffectsRender<SolidColorBrush>(ColorProperty);
         }
 

+ 1 - 0
src/Avalonia.Visuals/Media/Transform.cs

@@ -3,6 +3,7 @@
 
 using System;
 using Avalonia.Animation;
+using Avalonia.Animation.Animators;
 using Avalonia.VisualTree;
 
 namespace Avalonia.Media

+ 10 - 4
src/Avalonia.Visuals/Point.cs

@@ -2,6 +2,7 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System.Globalization;
+using Avalonia.Animation.Animators;
 using Avalonia.Utilities;
 
 namespace Avalonia
@@ -11,6 +12,11 @@ namespace Avalonia
     /// </summary>
     public readonly struct Point
     {
+        static Point()
+        {
+            Animation.Animation.RegisterAnimator<PointAnimator>(prop => typeof(Point).IsAssignableFrom(prop.PropertyType));
+        }
+
         /// <summary>
         /// The X position.
         /// </summary>
@@ -133,7 +139,7 @@ namespace Avalonia
         /// <param name="p">Point to multiply</param>
         /// <param name="k">Factor</param>
         /// <returns>Points having its coordinates multiplied</returns>
-        public static Point operator *(Point p, double k) => new Point(p.X*k, p.Y*k);
+        public static Point operator *(Point p, double k) => new Point(p.X * k, p.Y * k);
 
         /// <summary>
         /// Multiplies a point by a factor coordinate-wise
@@ -141,7 +147,7 @@ namespace Avalonia
         /// <param name="p">Point to multiply</param>
         /// <param name="k">Factor</param>
         /// <returns>Points having its coordinates multiplied</returns>
-        public static Point operator *(double k, Point p) => new Point(p.X*k, p.Y*k);
+        public static Point operator *(double k, Point p) => new Point(p.X * k, p.Y * k);
 
         /// <summary>
         /// Divides a point by a factor coordinate-wise
@@ -149,8 +155,8 @@ namespace Avalonia
         /// <param name="p">Point to divide by</param>
         /// <param name="k">Factor</param>
         /// <returns>Points having its coordinates divided</returns>
-        public static Point operator /(Point p, double k) => new Point(p.X/k, p.Y/k);
-         
+        public static Point operator /(Point p, double k) => new Point(p.X / k, p.Y / k);
+
         /// <summary>
         /// Applies a matrix to a point.
         /// </summary>

+ 6 - 0
src/Avalonia.Visuals/Rect.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Globalization;
+using Avalonia.Animation.Animators;
 using Avalonia.Utilities;
 
 namespace Avalonia
@@ -12,6 +13,11 @@ namespace Avalonia
     /// </summary>
     public readonly struct Rect
     {
+        static Rect()
+        {
+            Animation.Animation.RegisterAnimator<RectAnimator>(prop => typeof(Rect).IsAssignableFrom(prop.PropertyType));
+        }
+
         /// <summary>
         /// An empty rectangle.
         /// </summary>

+ 6 - 0
src/Avalonia.Visuals/Size.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Globalization;
+using Avalonia.Animation.Animators;
 using Avalonia.Utilities;
 
 namespace Avalonia
@@ -12,6 +13,11 @@ namespace Avalonia
     /// </summary>
     public readonly struct Size
     {
+        static Size()
+        {
+            Animation.Animation.RegisterAnimator<SizeAnimator>(prop => typeof(Size).IsAssignableFrom(prop.PropertyType));
+        }
+
         /// <summary>
         /// A size representing infinity.
         /// </summary>

+ 38 - 1
src/Avalonia.Visuals/Thickness.cs

@@ -3,6 +3,8 @@
 
 using System;
 using System.Globalization;
+using Avalonia.Animation;
+using Avalonia.Animation.Animators;
 using Avalonia.Utilities;
 
 namespace Avalonia
@@ -12,6 +14,11 @@ namespace Avalonia
     /// </summary>
     public readonly struct Thickness
     {
+        static Thickness()
+        {
+            Animation.Animation.RegisterAnimator<ThicknessAnimator>(prop => typeof(Thickness).IsAssignableFrom(prop.PropertyType));
+        }
+
         /// <summary>
         /// The thickness on the left.
         /// </summary>
@@ -134,6 +141,36 @@ namespace Avalonia
                 a.Bottom + b.Bottom);
         }
 
+        /// <summary>
+        /// Subtracts two Thicknesses.
+        /// </summary>
+        /// <param name="a">The first thickness.</param>
+        /// <param name="b">The second thickness.</param>
+        /// <returns>The equality.</returns>
+        public static Thickness operator -(Thickness a, Thickness b)
+        {
+            return new Thickness(
+                a.Left - b.Left,
+                a.Top - b.Top,
+                a.Right - b.Right,
+                a.Bottom - b.Bottom);
+        }
+
+        /// <summary>
+        /// Multiplies a Thickness to a scalar.
+        /// </summary>
+        /// <param name="a">The thickness.</param>
+        /// <param name="b">The scalar.</param>
+        /// <returns>The equality.</returns>
+        public static Thickness operator *(Thickness a, double b)
+        {
+            return new Thickness(
+                a.Left * b,
+                a.Top * b,
+                a.Right * b,
+                a.Bottom * b);
+        }
+
         /// <summary>
         /// Adds a Thickness to a Size.
         /// </summary>
@@ -169,7 +206,7 @@ namespace Avalonia
         {
             using (var tokenizer = new StringTokenizer(s, CultureInfo.InvariantCulture, exceptionMessage: "Invalid Thickness"))
             {
-                if(tokenizer.TryReadDouble(out var a))
+                if (tokenizer.TryReadDouble(out var a))
                 {
                     if (tokenizer.TryReadDouble(out var b))
                     {

+ 8 - 2
src/Avalonia.Visuals/Vector.cs

@@ -3,6 +3,7 @@
 
 using System;
 using System.Globalization;
+using Avalonia.Animation.Animators;
 using JetBrains.Annotations;
 
 namespace Avalonia
@@ -12,6 +13,11 @@ namespace Avalonia
     /// </summary>
     public readonly struct Vector
     {
+        static Vector()
+        {
+            Animation.Animation.RegisterAnimator<VectorAnimator>(prop => typeof(Vector).IsAssignableFrom(prop.PropertyType));
+        }
+
         /// <summary>
         /// The X vector.
         /// </summary>
@@ -60,7 +66,7 @@ namespace Avalonia
         /// <returns>The dot product</returns>
         public static double operator *(Vector a, Vector b)
         {
-            return a.X*b.X + a.Y*b.Y;
+            return a.X * b.X + a.Y * b.Y;
         }
 
         /// <summary>
@@ -88,7 +94,7 @@ namespace Avalonia
         /// <summary>
         /// Length of the vector
         /// </summary>
-        public double Length => Math.Sqrt(X*X + Y*Y);
+        public double Length => Math.Sqrt(X * X + Y * Y);
 
         /// <summary>
         /// Negates a vector.

+ 1 - 1
src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs

@@ -137,7 +137,7 @@ namespace Avalonia.Skia
         {
             var colorType = PixelFormatHelper.ResolveColorType(format);
 
-            return new SKImageInfo(width, height, colorType, SKAlphaType.Premul);
+            return new SKImageInfo(Math.Max(width, 1), Math.Max(height, 1), colorType, SKAlphaType.Premul);
         }
 
         /// <summary>

+ 77 - 0
tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs

@@ -0,0 +1,77 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Styling;
+using Avalonia.UnitTests;
+using Avalonia.Data;
+using Xunit;
+
+namespace Avalonia.Animation.UnitTests
+{
+    public class AnimationIterationTests
+    {
+        [Fact]
+        public void Check_Initial_Inter_and_Trailing_Delay_Values()
+        {
+            var keyframe1 = new KeyFrame()
+            {
+                Setters =
+                {
+                    new Setter(Border.WidthProperty, 200d),
+                },
+                Cue = new Cue(1d)
+            };
+
+            var keyframe2 = new KeyFrame()
+            {
+                Setters =
+                {
+                    new Setter(Border.WidthProperty, 100d),
+                },
+                Cue = new Cue(0d)
+            };
+
+            var animation = new Animation()
+            {
+                Duration = TimeSpan.FromSeconds(3),
+                Delay = TimeSpan.FromSeconds(3),
+                DelayBetweenIterations = TimeSpan.FromSeconds(3),
+                IterationCount = new IterationCount(2),
+                Children =
+                {
+                    keyframe2,
+                    keyframe1
+                }
+            };
+
+            var border = new Border()
+            {
+                Height = 100d,
+                Width = 100d
+            };
+
+            var clock = new TestClock();
+            var animationRun = animation.RunAsync(border, clock);
+
+            clock.Step(TimeSpan.Zero);
+
+            // Initial Delay.
+            clock.Step(TimeSpan.FromSeconds(1));
+            Assert.Equal(border.Width, 0d);
+
+            clock.Step(TimeSpan.FromSeconds(6));
+
+            // First Inter-Iteration delay.
+            clock.Step(TimeSpan.FromSeconds(8));
+            Assert.Equal(border.Width, 200d);
+
+            // Trailing Delay should be non-existent.
+            clock.Step(TimeSpan.FromSeconds(14));
+            Assert.True(animationRun.Status == TaskStatus.RanToCompletion);
+            Assert.Equal(border.Width, 100d);
+        }
+    }
+}

+ 25 - 0
tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj

@@ -0,0 +1,25 @@
+<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
+  <PropertyGroup>
+    <TargetFrameworks>netcoreapp2.0</TargetFrameworks>
+    <OutputType>Library</OutputType>
+  </PropertyGroup>
+  <Import Project="..\..\build\UnitTests.NetCore.targets" />
+  <Import Project="..\..\build\Moq.props" />
+  <Import Project="..\..\build\XUnit.props" />
+  <Import Project="..\..\build\Rx.props" />
+  <Import Project="..\..\build\Microsoft.Reactive.Testing.props" />
+  <ItemGroup>
+    <ProjectReference Include="..\..\src\Avalonia.Animation\Avalonia.Animation.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Controls\Avalonia.Controls.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Input\Avalonia.Input.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Interactivity\Avalonia.Interactivity.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Layout\Avalonia.Layout.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Visuals\Avalonia.Visuals.csproj" />
+    <ProjectReference Include="..\..\src\Avalonia.Styling\Avalonia.Styling.csproj" />
+    <ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
+  </ItemGroup>
+  <ItemGroup>
+    <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
+  </ItemGroup>
+</Project>

+ 10 - 0
tests/Avalonia.Animation.UnitTests/Properties/AssemblyInfo.cs

@@ -0,0 +1,10 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Reflection;
+using Xunit;
+
+[assembly: AssemblyTitle("Avalonia.Animation.UnitTests")]
+
+// Don't run tests in parallel.
+[assembly: CollectionBehavior(DisableTestParallelization = true)]

+ 28 - 0
tests/Avalonia.Animation.UnitTests/TestClock.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Animation.UnitTests
+{
+    internal class TestClock : IClock, IDisposable
+    {
+        private IObserver<TimeSpan> _observer;
+
+        public PlayState PlayState { get; set; } = PlayState.Run;
+        
+        public void Dispose()
+        {
+            _observer?.OnCompleted();
+        }
+
+        public void Step(TimeSpan time)
+        {
+            _observer?.OnNext(time);
+        }
+
+        public IDisposable Subscribe(IObserver<TimeSpan> observer)
+        {
+            _observer = observer;
+            return this;
+        }
+    }
+}