Search code examples
wpfpathgeometrydoubleanimation

Change path of already started DoubleAnimationUsingPath


Is it possible to change the path, e.g. the PathGeometry property, of a DoubleAnimationUsingPath while the animation is already in progress? If so, how?

Some code of mine:

XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <Canvas x:Name="BackgroundCanvas"
                Background="Transparent"
                Grid.ColumnSpan="2">
            <Ellipse Fill="Black" Width="10" Height="10" x:Name="Circ">
                <Ellipse.RenderTransform>
                    <TranslateTransform X="-5" Y="-5" />
                </Ellipse.RenderTransform>
            </Ellipse>
        </Canvas>

        <Rectangle x:Name="LeftRect" Width="100" Height="100" Grid.Column="0" Fill="#80002EE2" />
        <Rectangle x:Name="RightRect" Width="100" Height="100" Grid.Column="1" Fill="#8000B70A" />
    </Grid>
</Window>

Code-behind:

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    Func<FrameworkElement, Point> centerOf = ele => ele.TransformToVisual(BackgroundCanvas).Transform(new Point(ele.Width/2, ele.Height/2));
    Point start = centerOf(LeftRect);
    Point end = centerOf(RightRect);

    LineGeometry geom = new LineGeometry(start, end);

    var animation = new DoubleAnimationUsingPath
    {
        Duration = Duration.Automatic,
        PathGeometry = PathGeometry.CreateFromGeometry(geom)
    };

    animation.Source = PathAnimationSource.X;
    Circ.BeginAnimation(Canvas.LeftProperty, animation);

    animation.Completed += delegate
    {
        Window_Loaded(null, null);
    };
    animation.Source = PathAnimationSource.Y;
    Circ.BeginAnimation(Canvas.TopProperty, animation);
}

This should move a 10x10 circle between the center of two rectangles. I want to change the animation if it wasn't completed yet and LayoutUpdated fires, so that the animation actually ends at the center of a rectangle when the window is e.g. resized.


Solution

  • here is what I attempted

    I added a shape to the canvas and when you click anywhere in the canvas the shape will move towards the mouse pointer. you can click anywhere else again in the canvas and the shape will change its path to follow the new pointer location.

    xaml

    <Canvas x:Name="BackgroundCanvas"
            Background="Transparent"
            PreviewMouseDown="BackgroundCanvas_PreviewMouseDown">
        <Ellipse Fill="Black"
                 Width="10"
                 Height="10"
                 x:Name="circ">
            <Ellipse.RenderTransform>
                <TranslateTransform X="-5"
                                    Y="-5" />
            </Ellipse.RenderTransform>
        </Ellipse>
    </Canvas>
    

    BackgroundCanvas_PreviewMouseDown

    private void BackgroundCanvas_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        double sx = (double)circ.GetValue(Canvas.LeftProperty);
        double sy = (double)circ.GetValue(Canvas.TopProperty);
        Point tp = e.GetPosition(BackgroundCanvas);
    
        if (double.IsNaN(sx))
            sx = 0;
        if (double.IsNaN(sy))
            sy = 0;
    
        LineGeometry geom = new LineGeometry(new Point(sx, sy), tp);
    
        Path p = new Path() { Data = geom, Stroke = Brushes.Black };
        BackgroundCanvas.Children.Add(p);
    
        var animation = new DoubleAnimationUsingPath
        {
            Duration = Duration.Automatic,
            PathGeometry = PathGeometry.CreateFromGeometry(geom)
        };
    
        animation.Source = PathAnimationSource.X;
        circ.BeginAnimation(Canvas.LeftProperty, animation);
        animation.Source = PathAnimationSource.Y;
        circ.BeginAnimation(Canvas.TopProperty, animation);
    }
    

    result

    result

    I added the path to show the result. give it a try and see how close it is


    EDIT

    I also attempted to smooth out the animation path so it does not look like straight lines

        Point pp;
        private void BackgroundCanvas_PreviewMouseDown(object sender, MouseButtonEventArgs e)
        {
            double sx = (double)circ.GetValue(Canvas.LeftProperty);
            double sy = (double)circ.GetValue(Canvas.TopProperty);
            Point tp = e.GetPosition(BackgroundCanvas);
    
            if (double.IsNaN(sx))
                sx = 0;
            if (double.IsNaN(sy))
                sy = 0;
            Point sp = new Point(sx, sy);
            StreamGeometry geom = new StreamGeometry();
            using (StreamGeometryContext ctx = geom.Open())
            {
                ctx.BeginFigure(sp, false, false);
                ctx.BezierTo(pp, tp, tp, true, false);
            }
            geom.Freeze();
    
            pp = tp;
    
            Path p = new Path() { Data = geom, Stroke = Brushes.Black };
            BackgroundCanvas.Children.Add(p);
    
            var animation = new DoubleAnimationUsingPath
            {
                Duration = Duration.Automatic,
                PathGeometry = PathGeometry.CreateFromGeometry(geom)
            };
    
            animation.Source = PathAnimationSource.X;
            circ.BeginAnimation(Canvas.LeftProperty, animation);
            animation.Source = PathAnimationSource.Y;
            circ.BeginAnimation(Canvas.TopProperty, animation);
        }
    

    result

    result

    I hope this can solve your issue to modify the path of a running animation (not actually modifying but starting a new animation from the point it is triggered) with smoothness for blended-in appearance


    EDIT 2

    I combined my approach with your code and now when the window size will change the circle will follow a new path to the second rectangle.

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            Func<FrameworkElement, Point> centerOf = ele => ele.TransformToVisual(BackgroundCanvas).Transform(new Point(ele.Width / 2, ele.Height / 2));
            Point start = centerOf(LeftRect);
            Point end = centerOf(RightRect);
    
            LineGeometry geom = new LineGeometry(start, end);
            pp = end;
            var animation = new DoubleAnimationUsingPath
            {
                Duration = TimeSpan.FromMilliseconds(totalDuration),
                PathGeometry = PathGeometry.CreateFromGeometry(geom)
            };
    
            animation.Source = PathAnimationSource.X;
            Circ.BeginAnimation(Canvas.LeftProperty, animation);
    
            animation.Completed += delegate { Window_Loaded(null, null); };
    
            animation.Source = PathAnimationSource.Y;
            Circ.BeginAnimation(Canvas.TopProperty, animation);
            startTime = DateTime.Now;
            started = true;
        }
    
        Point pp;
        DateTime startTime;
        double totalDuration = 5000;
        bool started;
        private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (!started)
                return;
    
            Func<FrameworkElement, Point> centerOf = ele => ele.TransformToVisual(BackgroundCanvas).Transform(new Point(ele.Width / 2, ele.Height / 2));
            double sx = (double)Circ.GetValue(Canvas.LeftProperty);
            double sy = (double)Circ.GetValue(Canvas.TopProperty);
            Point tp = centerOf(RightRect);
    
            double timeLeft = totalDuration - DateTime.Now.Subtract(startTime).TotalMilliseconds;
    
            if (timeLeft < 1) return;
    
            if (double.IsNaN(sx))
                sx = 0;
            if (double.IsNaN(sy))
                sy = 0;
            Point sp = new Point(sx, sy);
            StreamGeometry geom = new StreamGeometry();
            using (StreamGeometryContext ctx = geom.Open())
            {
                ctx.BeginFigure(sp, false, false);
                ctx.BezierTo(pp, pp, tp, true, false);
            }
            geom.Freeze();
    
            pp = tp;
    
            var animation = new DoubleAnimationUsingPath
            {
                Duration = TimeSpan.FromMilliseconds(timeLeft),
                PathGeometry = PathGeometry.CreateFromGeometry(geom)
            };
    
            animation.Source = PathAnimationSource.X;
            Circ.BeginAnimation(Canvas.LeftProperty, animation);
    
            animation.Completed += delegate { Window_Loaded(null, null); };
    
            animation.Source = PathAnimationSource.Y;
            Circ.BeginAnimation(Canvas.TopProperty, animation);
        }
    

    see if this is what you are looking for. currently circle will use its current position and second rectangle to create it's path. if you wish to move the circle as window gets resized then perhaps we may need to implement storyboard and use seek methods to achieve the same.