Quellcode durchsuchen

Checkbox path morph

Krzysztof Krysiński vor 2 Jahren
Ursprung
Commit
0d48d895c5

+ 20 - 0
src/PixiEditor.UI.Common/Animators/MorphAnimator.cs

@@ -0,0 +1,20 @@
+// https://github.com/wieslawsoltes/MorphingDemo/blob/main/MorphingDemo/Avalonia/MorphAnimator.cs
+using Avalonia.Animation;
+using Avalonia.Media;
+using PixiEditor.UI.Common.Extensions;
+using PixiEditor.UI.Common.Utilities;
+
+namespace PixiEditor.UI.Common.Animators
+{
+    public class MorphAnimator : InterpolatingAnimator<Geometry>
+    {
+        public override Geometry Interpolate(double progress, Geometry oldValue, Geometry newValue)
+        {
+            var clone = (oldValue as PathGeometry).ClonePathGeometry();
+
+            Morph.To(clone, newValue as PathGeometry, progress);
+
+            return clone;
+        }
+    }
+}

+ 18 - 0
src/PixiEditor.UI.Common/Animators/MorphTransition.cs

@@ -0,0 +1,18 @@
+using Avalonia.Animation;
+using Avalonia.Media;
+using PixiEditor.UI.Common.Extensions;
+using PixiEditor.UI.Common.Utilities;
+
+namespace PixiEditor.UI.Common.Animators;
+
+public class MorphTransition : InterpolatingTransitionBase<Geometry>
+{
+    protected override Geometry Interpolate(double progress, Geometry from, Geometry to)
+    {
+        var clone = (from as PathGeometry).ClonePathGeometry();
+
+        Morph.To(clone, to as PathGeometry, progress);
+
+        return clone;
+    }
+}

+ 1 - 0
src/PixiEditor.UI.Common/Controls/Button.axaml

@@ -26,6 +26,7 @@
                                   Content="{TemplateBinding Content}"
                                   ContentTemplate="{TemplateBinding ContentTemplate}"
                                   CornerRadius="{TemplateBinding CornerRadius}"
+                                  Transitions="{TemplateBinding Transitions}"
                                   RecognizesAccessKey="True"
                                   TextElement.Foreground="{TemplateBinding Foreground}" />
             </ControlTemplate>

+ 33 - 13
src/PixiEditor.UI.Common/Controls/CheckBox.axaml

@@ -1,5 +1,6 @@
 <ResourceDictionary xmlns="https://github.com/avaloniaui"
-        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:animators="clr-namespace:PixiEditor.UI.Common.Animators">
     <Design.PreviewWith>
         <StackPanel HorizontalAlignment="Center">
             <CheckBox IsChecked="True" Content="Label"/>
@@ -8,6 +9,29 @@
         </StackPanel>
     </Design.PreviewWith>
 
+    <PathGeometry x:Key="Tick">
+        <PathGeometry.Figures>
+            <PathFigure StartPoint="0 4" IsClosed="False">
+                <LineSegment Point="3 8"/>
+                <LineSegment Point="8 0"/>
+            </PathFigure>
+        </PathGeometry.Figures>
+    </PathGeometry>
+
+    <PathGeometry x:Key="Intermediate">
+        <PathGeometry.Figures>
+            <PathFigure StartPoint="0 8">
+                <LineSegment Point="8 0"/>
+            </PathFigure>
+        </PathGeometry.Figures>
+    </PathGeometry>
+
+    <PathGeometry x:Key="Empty">
+        <PathFigure StartPoint="0 4">
+            <LineSegment Point="0 4"/>
+        </PathFigure>
+    </PathGeometry>
+
      <ControlTheme x:Key="{x:Type CheckBox}"
                 TargetType="CheckBox">
     <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
@@ -25,13 +49,10 @@
                  Background="{DynamicResource ThemeControlLowColor}"
                                       BorderThickness="1">
             <Panel>
-                <Path FlowDirection="LeftToRight" Width="9" Height="9" x:Name="checkMark" Margin="1 1 0 0"
+                <Path FlowDirection="LeftToRight" Width="9" Height="9" x:Name="checkMark" Margin="2 2 0 0"
                       Stroke="{DynamicResource AccentColor}" StrokeThickness="1.5"
-                      Data="M 0 4 L 3 8 8 0" />
-              <Path x:Name="indeterminateMark"
-                    FlowDirection="LeftToRight" Width="9" Height="9" Margin="1 1 0 0"
-                    Stroke="{DynamicResource AccentColor}" StrokeThickness="1.5"
-                    Data="M 0 8 L 8 0" />
+                      Data="{StaticResource Empty}">
+                </Path>
             </Panel>
           </Border>
           <ContentPresenter Name="PART_ContentPresenter"
@@ -52,16 +73,15 @@
       <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}" />
     </Style>
     <Style Selector="^ /template/ Path#checkMark">
-      <Setter Property="IsVisible" Value="False" />
-    </Style>
-    <Style Selector="^ /template/ Path#indeterminateMark">
-      <Setter Property="IsVisible" Value="False" />
+      <Setter Property="Data" Value="{StaticResource Empty}"/>
     </Style>
     <Style Selector="^:checked /template/ Path#checkMark">
       <Setter Property="IsVisible" Value="True" />
+      <Setter Property="Data" Value="{StaticResource Tick}"/>
     </Style>
-    <Style Selector="^:indeterminate /template/ Path#indeterminateMark">
-      <Setter Property="IsVisible" Value="True" />
+    <Style Selector="^:indeterminate /template/ Path#checkMark">
+        <Setter Property="IsVisible" Value="True" />
+        <Setter Property="Data" Value="{StaticResource Intermediate}"/>
     </Style>
     <Style Selector="^:disabled /template/ Border#border">
       <Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}" />

+ 1 - 0
src/PixiEditor.UI.Common/Controls/RepeatButton.axaml

@@ -23,6 +23,7 @@
                           Content="{TemplateBinding Content}"
                           ContentTemplate="{TemplateBinding ContentTemplate}"
                           CornerRadius="{TemplateBinding CornerRadius}"
+                          Transitions="{TemplateBinding Transitions}"
                           TextElement.Foreground="{TemplateBinding Foreground}" />
       </ControlTemplate>
     </Setter>

+ 1 - 0
src/PixiEditor.UI.Common/Controls/ToggleButton.axaml

@@ -21,6 +21,7 @@
                           BorderBrush="{TemplateBinding BorderBrush}"
                           BorderThickness="{TemplateBinding BorderThickness}"
                           Content="{TemplateBinding Content}"
+                          Transitions="{TemplateBinding Transitions}"
                           ContentTemplate="{TemplateBinding ContentTemplate}"
                           CornerRadius="{TemplateBinding CornerRadius}"
                           RecognizesAccessKey="True"

+ 328 - 0
src/PixiEditor.UI.Common/Extensions/PathGeometryExtensions.cs

@@ -0,0 +1,328 @@
+// https://github.com/wieslawsoltes/MorphingDemo/blob/main/MorphingDemo/Avalonia/PathGeometryExtensions.cs
+
+using Avalonia;
+using Avalonia.Media;
+
+namespace PixiEditor.UI.Common.Extensions
+{
+    public static class PathGeometryExtensions
+    {
+        private static Point[] Interpolate(Point pt0, Point pt1)
+        {
+            var count = (int) Math.Max(1, Length(pt0, pt1));
+            var points = new Point[count];
+
+            for (var i = 0; i < count; i++)
+            {
+                var t = (i + 1d) / count;
+                var x = (1 - t) * pt0.X + t * pt1.X;
+                var y = (1 - t) * pt0.Y + t * pt1.Y;
+                points[i] = new Point(x, y);
+            }
+
+            return points;
+        }
+
+        private static Point[] FlattenCubic(Point pt0, Point pt1, Point pt2, Point pt3)
+        {
+            var count = (int) Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2) + Length(pt2, pt3));
+            var points = new Point[count];
+
+            for (var i = 0; i < count; i++)
+            {
+                var t = (i + 1d) / count;
+                var x = (1 - t) * (1 - t) * (1 - t) * pt0.X +
+                        3 * t * (1 - t) * (1 - t) * pt1.X +
+                        3 * t * t * (1 - t) * pt2.X +
+                        t * t * t * pt3.X;
+                var y = (1 - t) * (1 - t) * (1 - t) * pt0.Y +
+                        3 * t * (1 - t) * (1 - t) * pt1.Y +
+                        3 * t * t * (1 - t) * pt2.Y +
+                        t * t * t * pt3.Y;
+                points[i] = new Point(x, y);
+            }
+
+            return points;
+        }
+
+        private static Point[] FlattenQuadratic(Point pt0, Point pt1, Point pt2)
+        {
+            var count = (int) Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
+            var points = new Point[count];
+
+            for (var i = 0; i < count; i++)
+            {
+                var t = (i + 1d) / count;
+                var x = (1 - t) * (1 - t) * pt0.X + 2 * t * (1 - t) * pt1.X + t * t * pt2.X;
+                var y = (1 - t) * (1 - t) * pt0.Y + 2 * t * (1 - t) * pt1.Y + t * t * pt2.Y;
+                points[i] = new Point(x, y);
+            }
+
+            return points;
+        }
+
+        private static Point[] FlattenConic(Point pt0, Point pt1, Point pt2, float weight)
+        {
+            var count = (int) Math.Max(1, Length(pt0, pt1) + Length(pt1, pt2));
+            var points = new Point[count];
+
+            for (var i = 0; i < count; i++)
+            {
+                var t = (i + 1d) / count;
+                var denominator = (1 - t) * (1 - t) + 2 * weight * t * (1 - t) + t * t;
+                var x = (1 - t) * (1 - t) * pt0.X + 2 * weight * t * (1 - t) * pt1.X + t * t * pt2.X;
+                var y = (1 - t) * (1 - t) * pt0.Y + 2 * weight * t * (1 - t) * pt1.Y + t * t * pt2.Y;
+                x /= denominator;
+                y /= denominator;
+                points[i] = new Point(x, y);
+            }
+
+            return points;
+        }
+
+        private static double Length(Point pt0, Point pt1)
+        {
+            return Math.Sqrt(Math.Pow(pt1.X - pt0.X, 2) + Math.Pow(pt1.Y - pt0.Y, 2));
+        }
+
+        public static PathGeometry GetFlattenedPathGeometry(this PathGeometry pathIn)
+        {
+            var pathOut = new PathGeometry()
+            {
+                FillRule = pathIn.FillRule
+            };
+
+            foreach (var figureIn in pathIn.Figures)
+            {
+                var firstPoint = new Point();
+                var lastPoint = new Point();
+                var figureOut = new PathFigure()
+                {
+                    IsClosed = figureIn.IsClosed,
+                    IsFilled = figureIn.IsFilled,
+                    StartPoint = figureIn.StartPoint
+                };
+                firstPoint = lastPoint = figureIn.StartPoint;
+
+                pathOut.Figures.Add(figureOut);
+
+                if (figureIn.Segments is null)
+                {
+                    continue;
+                }
+
+                var polyLineSegmentOut = new PolyLineSegment();
+
+                foreach (var pathSegmentIn in figureIn.Segments)
+                {
+                    switch (pathSegmentIn)
+                    {
+                        case ArcSegment arcSegmentIn:
+                        {
+                            // TODO:
+                        }
+                            break;
+                        case BezierSegment bezierSegmentIn:
+                        {
+                            var points = FlattenCubic(lastPoint, bezierSegmentIn.Point1, bezierSegmentIn.Point2, bezierSegmentIn.Point3);
+                            
+                            for (var i = 0; i < points.Length; i++)
+                            {
+                                polyLineSegmentOut.Points?.Add(points[i]);
+                            }
+
+                            lastPoint = bezierSegmentIn.Point3;
+                        }
+                            break;
+                        case LineSegment lineSegmentIn:
+                        {
+                            var points = Interpolate(lastPoint, lineSegmentIn.Point);
+
+                            for (var i = 0; i < points.Length; i++)
+                            {
+                                polyLineSegmentOut.Points?.Add(points[i]);
+                            }
+
+                            lastPoint = lineSegmentIn.Point;
+                        }
+                            break;
+                        case QuadraticBezierSegment quadraticBezierSegmentIn:
+                        {
+                            var points = FlattenQuadratic(lastPoint, quadraticBezierSegmentIn.Point1, quadraticBezierSegmentIn.Point2);
+
+                            for (var i = 0; i < points.Length; i++)
+                            {
+                                polyLineSegmentOut.Points?.Add(points[i]);
+                            }
+
+                            lastPoint = quadraticBezierSegmentIn.Point2;
+                        }
+                            break;
+                        case PolyLineSegment polyLineSegmentIn:
+                        {
+                            if (polyLineSegmentIn.Points.Count > 0)
+                            {
+                                for (var i = 0; i < polyLineSegmentIn.Points.Count; i++)
+                                {
+                                    var points = Interpolate(lastPoint, polyLineSegmentIn.Points[i]);
+                                    for (int j = 0; j < points.Length; j++)
+                                    {
+                                        polyLineSegmentOut.Points?.Add(points[j]);
+                                    }
+                                    lastPoint = polyLineSegmentIn.Points[i];
+                                }
+                            }
+                        }
+                            break;
+                        default:
+                        {
+                            // TODO:
+                        }
+                            break;
+                    }
+                }
+#if true
+                if (figureIn.IsClosed)
+                {
+                    var points = Interpolate(lastPoint, firstPoint);
+
+                    for (var i = 0; i < points.Length; i++)
+                    {
+                        polyLineSegmentOut.Points?.Add(points[i]);
+                    }
+
+                    firstPoint = lastPoint = new Point(0, 0);
+                }
+#endif
+
+                if (polyLineSegmentOut.Points?.Count > 0)
+                {
+                    figureOut.Segments?.Add(polyLineSegmentOut);
+                }
+            }
+
+            return pathOut;
+        }
+
+        public static PathGeometry ToFlattenedPathGeometry(this IList<Point> sourcePoints)
+        {
+            var source = new PathGeometry
+            {
+                FillRule = FillRule.EvenOdd
+            };
+
+            var sourceFigure = new PathFigure()
+            {
+                IsClosed = false,
+                IsFilled = false,
+                StartPoint = sourcePoints.First()
+            };
+            source.Figures.Add(sourceFigure);
+
+            var polylineSegment = new PolyLineSegment(sourcePoints.Skip(1));
+            sourceFigure.Segments?.Add(polylineSegment);
+
+            return source;
+        }
+
+        public static PathGeometry ClonePathGeometry(this PathGeometry pathIn)
+        {
+            var pathOut = new PathGeometry()
+            {
+                FillRule = pathIn.FillRule
+            };
+
+            foreach (var figureIn in pathIn.Figures)
+            {
+                var figureOut = figureIn.ClonePathFigure();
+                pathOut.Figures.Add(figureOut);
+            }
+
+            return pathOut;
+        }
+
+        public static PathFigure ClonePathFigure(this PathFigure figureIn)
+        {
+            var figureOut = new PathFigure()
+            {
+                IsClosed = figureIn.IsClosed,
+                IsFilled = figureIn.IsFilled,
+                StartPoint = figureIn.StartPoint
+            };
+
+            if (figureIn.Segments is null)
+            {
+                return figureOut;
+            }
+
+            foreach (var pathSegmentIn in figureIn.Segments)
+            {
+                switch (pathSegmentIn)
+                {
+                    case ArcSegment arcSegmentIn:
+                    {
+                        var arcSegmentOut = new ArcSegment()
+                        {
+                            IsLargeArc = arcSegmentIn.IsLargeArc,
+                            Point = arcSegmentIn.Point,
+                            RotationAngle = arcSegmentIn.RotationAngle,
+                            Size = arcSegmentIn.Size,
+                            SweepDirection = arcSegmentIn.SweepDirection
+                        };
+                        figureOut.Segments?.Add(arcSegmentOut);
+                    }
+                        break;
+                    case BezierSegment bezierSegmentIn:
+                    {
+                        var bezierSegmentOut = new BezierSegment()
+                        {
+                            Point1 = bezierSegmentIn.Point1,
+                            Point2 = bezierSegmentIn.Point2,
+                            Point3 = bezierSegmentIn.Point3
+                        };
+                        figureOut.Segments?.Add(bezierSegmentOut);
+                    }
+                        break;
+                    case LineSegment lineSegmentIn:
+                    {
+                        var lineSegmentOut = new LineSegment()
+                        {
+                            Point = lineSegmentIn.Point
+                        };
+                        figureOut.Segments?.Add(lineSegmentOut);
+                    }
+                        break;
+                    case QuadraticBezierSegment quadraticBezierSegmentIn:
+                    {
+                        var quadraticBezierSegmentOut = new QuadraticBezierSegment()
+                        {
+                            Point1 = quadraticBezierSegmentIn.Point1,
+                            Point2 = quadraticBezierSegmentIn.Point2,
+                        };
+                        figureOut.Segments?.Add(quadraticBezierSegmentOut);
+                    }
+                        break;
+                    case PolyLineSegment polyLineSegmentIn:
+                    {
+                        var polyLineSegmentOut = new PolyLineSegment();
+
+                        foreach (var pt in polyLineSegmentIn.Points)
+                        {
+                            polyLineSegmentOut.Points?.Add(pt);
+                        }
+
+                        figureOut.Segments?.Add(polyLineSegmentOut);
+                    }
+                        break;
+                    default:
+                    {
+                        // TODO:
+                    }
+                        break;
+                }
+            }
+
+            return figureOut;
+        }
+    }
+}

+ 5 - 0
src/PixiEditor.UI.Common/Styles/PixiEditor.Styles.axaml

@@ -0,0 +1,5 @@
+<Styles xmlns="https://github.com/avaloniaui"
+                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+    <StyleInclude Source="avares://PixiEditor.UI.Common/Styles/TextStyles.axaml"/>
+    <StyleInclude Source="avares://PixiEditor.UI.Common/Styles/Transitions.axaml"/>
+</Styles>

+ 41 - 0
src/PixiEditor.UI.Common/Styles/Transitions.axaml

@@ -0,0 +1,41 @@
+<Styles xmlns="https://github.com/avaloniaui"
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+        xmlns:animators="clr-namespace:PixiEditor.UI.Common.Animators">
+    <Design.PreviewWith>
+        <Border Padding="20">
+            <!-- Add Controls for Previewer Here -->
+        </Border>
+    </Design.PreviewWith>
+
+    <Style Selector="Button">
+        <Setter Property="Transitions">
+            <Transitions>
+                <BrushTransition Property="Background" Duration="0:0:0.1"/>
+            </Transitions>
+        </Setter>
+    </Style>
+
+    <Style Selector="RepeatButton">
+        <Setter Property="Transitions">
+            <Transitions>
+                <BrushTransition Property="Background" Duration="0:0:0.1"/>
+            </Transitions>
+        </Setter>
+    </Style>
+
+    <Style Selector="ToggleButton">
+        <Setter Property="Transitions">
+            <Transitions>
+                <BrushTransition Property="Background" Duration="0:0:0.1"/>
+            </Transitions>
+        </Setter>
+    </Style>
+
+    <Style Selector="CheckBox /template/ Path#checkMark">
+        <Setter Property="Transitions">
+            <Transitions>
+                <animators:MorphTransition Property="Data" Duration="0:0:0.1"/>
+            </Transitions>
+        </Setter>
+    </Style>
+</Styles>

+ 1 - 1
src/PixiEditor.UI.Common/Themes/PixiEditorTheme.axaml

@@ -10,5 +10,5 @@
     </Styles.Resources>
 
     <StyleInclude Source="/Styles/PixiEditor.Controls.axaml"/>
-    <StyleInclude Source="/Styles/TextStyles.axaml"/>
+    <StyleInclude Source="/Styles/PixiEditor.Styles.axaml"/>
 </Styles>

+ 5 - 1
src/PixiEditor.UI.Common/Themes/PixiEditorTheme.axaml.cs

@@ -1,5 +1,8 @@
-using Avalonia.Markup.Xaml;
+using Avalonia.Animation;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
 using Avalonia.Styling;
+using PixiEditor.UI.Common.Animators;
 
 namespace PixiEditor.UI.Common.Themes;
 
@@ -7,6 +10,7 @@ public class PixiEditorTheme : Styles
 {
     public PixiEditorTheme(IServiceProvider? sp = null)
     {
+        Animation.RegisterCustomAnimator<Geometry, MorphAnimator>();
         AvaloniaXamlLoader.Load(sp, this);
     }
 }

+ 336 - 0
src/PixiEditor.UI.Common/Utilities/Morph.cs

@@ -0,0 +1,336 @@
+using Avalonia;
+using Avalonia.Animation.Easings;
+using Avalonia.Media;
+using PixiEditor.UI.Common.Extensions;
+
+namespace PixiEditor.UI.Common.Utilities
+{
+    public static class Morph
+    {
+        public static bool Collapse(PathGeometry sourceGeometry, double progress)
+        {
+            int count = sourceGeometry.Figures.Count;
+            for (int i = 0; i < sourceGeometry.Figures.Count; i++)
+            {
+                count -= MorphCollapse(sourceGeometry.Figures[i], progress);
+            }
+
+            if (count <= 0) return true;
+
+            return false;
+        }
+
+        private static void MoveFigure(PathFigure source, double p, double progress)
+        {
+            var segment = (PolyLineSegment)source.Segments[0];
+
+            for (int i = 0; i < segment.Points.Count; i++)
+            {
+                var fromX = segment.Points[i].X;
+                var fromY = segment.Points[i].Y;
+
+                var x = fromX + p;
+                segment.Points[i] = new Point(x, fromY);
+            }
+
+            var newX = source.StartPoint.X + p;
+
+            source.StartPoint = new Point(newX, source.StartPoint.Y);
+        }
+/* TODO:
+        private static bool DoFiguresOverlap(PathFigures figures, int index0, int index1, int index2)
+        {
+            if (index2 < figures.Count && index0 >= 0)
+            {
+                var g0 = new PathGeometry();
+                g0.Figures.Add(figures[index2]);
+                var g1 = new PathGeometry();
+                g1.Figures.Add(figures[index1]);
+                var g2 = new PathGeometry();
+                g2.Figures.Add(figures[index0]);
+                // TODO: https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.geometry.fillcontainswithdetail?view=net-5.0
+                var result0 = g0.FillContainsWithDetail(g1);
+                var result1 = g0.FillContainsWithDetail(g2);
+
+                return
+                    (result0 == IntersectionDetail.FullyContains ||
+                        result0 == IntersectionDetail.FullyInside) &&
+                    (result1 == IntersectionDetail.FullyContains ||
+                        result1 == IntersectionDetail.FullyInside);
+            }
+
+            return false;
+        }
+
+        private static bool DoFiguresOverlap(PathFigures figures, int index0, int index1)
+        {
+            if (index1 < figures.Count && index0 >= 0)
+            {
+                var g1 = new PathGeometry();
+                g1.Figures.Add(figures[index1]);
+                var g2 = new PathGeometry();
+                g2.Figures.Add(figures[index0]);
+                // TODO: https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.geometry.fillcontainswithdetail?view=net-5.0
+                var result = g1.FillContainsWithDetail(g2);
+                return result == IntersectionDetail.FullyContains || result == IntersectionDetail.FullyInside;
+            }
+            return false;
+        }
+*/
+        private static void CollapseFigure(PathFigure figure)
+        {
+            var points = ((PolyLineSegment)figure.Segments[0]).Points;
+            var centroid = GetCentroid(points);
+
+            for (int p = 0; p < points.Count; p++)
+            {
+                points[p] = centroid;
+            }
+
+            figure.StartPoint = centroid;
+        }
+
+        public static void To(PathGeometry sourceGeometry, PathGeometry geometry, Range sourceRange, double progress)
+        {
+            int k = 0;
+            for (int i = sourceRange.Start.Value; i < sourceRange.End.Value; i++)
+            {
+                MorphFigure(sourceGeometry.Figures[i], geometry.Figures[k], progress);
+                k++;
+            }
+        }
+
+        public static List<PathGeometry> ToCache(PathGeometry source, PathGeometry target, double speed, IEasing easing)
+        {
+            int steps = (int)(1 / speed);
+            double p = speed;
+            var cache = new List<PathGeometry>(steps);
+
+            // TODO: wasn't present in original
+            cache.Add(source.ClonePathGeometry());
+            
+            for (int i = 0; i < steps; i++)
+            {
+                var clone = source.ClonePathGeometry();
+                var easeP = easing.Ease(p);
+
+                To(clone, target, easeP);
+
+                p += speed;
+
+                cache.Add(clone);
+            }
+
+            // TODO: wasn't present in original
+            cache.Add(target.ClonePathGeometry());
+
+            return cache;
+        }
+
+        public static void To(PathGeometry source, PathGeometry target, double progress)
+        {
+            //
+            // Clone figures.
+            //
+            if (source.Figures.Count < target.Figures.Count)
+            {
+                var last = source.Figures.Last();
+                var toAdd = target.Figures.Count - source.Figures.Count;
+                for (int i = 0; i < toAdd; i++)
+                {
+                    var clone = last.ClonePathFigure();
+                    source.Figures.Add(clone);
+                }
+            }
+            //
+            // Contract the source, the problem here is that if we have a shape
+            // like 'O' where we need to cut a hole in a shape we will butcher such character
+            // since all excess shapes will be stored under this shape.
+            //
+            // We need to move and collapse them when moving.
+            // So lets collapse then to a single point.
+            //
+            else if (source.Figures.Count > target.Figures.Count)
+            {
+                var toAdd = source.Figures.Count - target.Figures.Count;
+                var lastIndex = target.Figures.Count - 1;
+
+                for (int i = 0; i < toAdd; i++)
+                {
+                    var clone = target.Figures[lastIndex].ClonePathFigure();
+                    //var clone = target.Figures[(lastIndex - (i % (lastIndex + 1)))].Clone();
+
+                    //
+                    // This is a temp solution but it works well for now.
+                    // We try to detect if our last shape has an overlapping geometry
+                    // if it does then we will clone the previrous shape.
+                    //
+                    if (lastIndex > 0)
+                    {
+                        /* TODO:
+                        if (DoFiguresOverlap(target.Figures, lastIndex - 1, lastIndex))
+                        {
+                            if (DoFiguresOverlap(target.Figures, lastIndex - 2, lastIndex - 1, lastIndex))
+                            {
+                                clone = target.Figures[lastIndex - 3].ClonePathFigure();
+                            }
+                            else if (lastIndex - 2 > 0)
+                            {
+                                clone = target.Figures[lastIndex - 2].ClonePathFigure();
+                            }
+                            else
+                            {
+                                CollapseFigure(clone);
+                            }
+                        }
+                        //*/
+                    }
+                    else
+                    {
+                        CollapseFigure(clone);
+                    }
+
+                    target.Figures.Add(clone);
+                }
+            }
+
+            int[] map = new int[source.Figures.Count];
+            for (int i = 0; i < map.Length; i++)
+                map[i] = -1;
+
+            //
+            // Morph Closest Figures.
+            //
+            for (int i = 0; i < source.Figures.Count; i++)
+            {
+                double closest = double.MaxValue;
+                int closestIndex = -1;
+
+                for (int j = 0; j < target.Figures.Count; j++)
+                {
+                    if (map.Contains(j))
+                        continue;
+                   
+                    var len = LengthSquared(source.Figures[i].StartPoint - target.Figures[j].StartPoint);
+                    if (len < closest)
+                    {
+                        closest = len;
+                        closestIndex = j;
+                    }
+                }
+
+                map[i] = closestIndex;
+            }
+
+            for (int i = 0; i < source.Figures.Count; i++)
+                MorphFigure(source.Figures[i], target.Figures[map[i]], progress);
+        }
+
+        private static double LengthSquared(Point point)
+        {
+            return point.X * point.X + point.Y*point.Y;
+        }
+        
+        public static void MorphFigure(PathFigure source, PathFigure target, double progress)
+        {
+            var sourceSegment = (LineSegment)source.Segments[0];
+            var targetSegment = (LineSegment)target.Segments[0];
+
+            //
+            // Interpolate from source to target.
+            //
+            if (progress >= 1)
+            {
+                var toX = targetSegment.Point.X;
+                var toY = targetSegment.Point.Y;
+                sourceSegment.Point = new Point(toX, toY);
+                source.StartPoint = new Point(target.StartPoint.X, target.StartPoint.Y);
+            }
+            else
+            {
+                var fromX = sourceSegment.Point.X;
+                var toX = targetSegment.Point.X;
+
+                var fromY = sourceSegment.Point.Y;
+                var toY = targetSegment.Point.Y;
+
+                if (fromX != toX || fromY != toY)
+                {
+                    var x = Interpolate(fromX, toX, progress);
+                    var y = Interpolate(fromY, toY, progress);
+                    sourceSegment.Point = new Point(x, y);
+                }
+
+                if (source.StartPoint.X != target.StartPoint.X || 
+                    source.StartPoint.Y != target.StartPoint.Y)
+                {
+                    var newX = Interpolate(source.StartPoint.X, target.StartPoint.X, progress);
+                    var newY = Interpolate(source.StartPoint.Y, target.StartPoint.Y, progress);
+                    source.StartPoint = new Point(newX, newY);
+                }
+            }
+        }
+
+        public static int MorphCollapse(PathFigure source, double progress)
+        {
+            var sourceSegment = (PolyLineSegment)source.Segments[0];
+
+            //
+            // Find Centroid
+            //
+            var centroid = GetCentroid(sourceSegment.Points);
+            for (int i = 0; i < sourceSegment.Points.Count; i++)
+            {
+                var fromX = sourceSegment.Points[i].X;
+                var toX = centroid.X;
+
+                var fromY = sourceSegment.Points[i].Y;
+                var toY = centroid.Y;
+
+                var x = Interpolate(fromX, toX, progress);
+                var y = Interpolate(fromY, toY, progress);
+
+                sourceSegment.Points[i] = new Point(x, y);
+            }
+
+            var newX = Interpolate(source.StartPoint.X, centroid.X, progress);
+            var newY = Interpolate(source.StartPoint.Y, centroid.Y, progress);
+
+            source.StartPoint = new Point(newX, newY);
+
+            if (centroid.X - newX < 0.005)
+            {
+                return 1;
+            }
+
+            return 0;
+        }
+
+        public static Point GetCentroid(IList<Point> nodes)
+        {
+            double x = 0, y = 0, area = 0, k;
+            Point a, b = nodes[nodes.Count - 1];
+
+            for (int i = 0; i < nodes.Count; i++)
+            {
+                a = nodes[i];
+
+                k = a.Y * b.X - a.X * b.Y;
+                area += k;
+                x += (a.X + b.X) * k;
+                y += (a.Y + b.Y) * k;
+
+                b = a;
+            }
+            area *= 3;
+
+            return (area == 0) ? new Point() : new Point(x /= area, y /= area);
+        }
+
+        public static double Interpolate(double from, double to, double progress)
+        {
+            return from + (to - from) * progress;
+        }
+    }
+
+}