1

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.

Sebastian Graf
  • 3,602
  • 3
  • 27
  • 38
  • you may need to stop the current animation and start a new animation, changing values are not possible due to freezable behavior. although path animation are bit tricky, let's see if there is a possibility. post your code for the same so let's get started. – pushpraj Aug 23 '14 at 13:38
  • I edited my question. How would I stop the animation and continue a new animation at the same time where I left off? – Sebastian Graf Aug 23 '14 at 13:50
  • Great! let's see how we can do it. in the meanwhile you may perhaps post the path samples, appreciated if you can post both, the original path and the alternate path. – pushpraj Aug 23 '14 at 14:00
  • I want to bind to the `Data` property of the `Path`. – Sebastian Graf Aug 23 '14 at 14:01
  • 1
    first of all the code has errors as `path.Data` is a `Geometry` as `casting as PathGeometry` would return `null`. this can be written as `var pg = PathGeometry.CreateFromGeometry(path.Data);`. secondly binding the Data property will not help in changing the animation while it is animating as the value is freezed before animation starts. last but not the least I am not able to guess the expected behavior from the question, could you please elaborate a bit. – pushpraj Aug 23 '14 at 14:48
  • `path.Data` is a `PathGeometry` for sure, because I set it myself, thus it won't return null. But for the sake of the example, I could even have an event that fired a new `PathGeometry` once in a while, for which the animation has to be adjusted. Basically, when it is fired, stop the current animation and the start the new at the timestep we left off. – Sebastian Graf Aug 23 '14 at 15:16
  • in short if I understand you correctly do you want to pause the animation and resume when triggered again? do you want to change the animation path as well? if so then does the new path will start or intersect from the point it is paused? – pushpraj Aug 24 '14 at 04:21
  • I don't want to visually pause the animation, but the animation should follow the changed `PathGeometry`, which will always have 7 nodes tracking some `FrameworkElement`s. The tracking works, so I have an event stream of `PathGeometry`, with which I want to update all running animations because otherwise they don't align with the nodes. Since animations are immutable once started, I suppose I should try to figure out how to stop an animation, get its time step, create a new animation based on the new path and set its time step to that of the previous animation. – Sebastian Graf Aug 24 '14 at 08:01

1 Answers1

1

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.

pushpraj
  • 13,458
  • 3
  • 33
  • 50
  • I appreciate your effort, but it does not. I also want to consider the time step where I left off, e.g. if the animation takes 3 seconds and if I click anywhere else after the first second, I want the new animation to take exactly 2 seconds. Actually it should be possible if I could figure out the time spent on the first animation. – Sebastian Graf Aug 24 '14 at 21:09
  • Appreciated if you can provide a working sample of what you have now, let's see how we can get through. – pushpraj Aug 25 '14 at 02:09
  • I edited my question to provide a more reduced example of what I mean. – Sebastian Graf Aug 25 '14 at 07:49
  • Great! I think that should be ok. you want to keep the time constant so the whole animation finishes in the predefined amount of time even if the path is changed. I'll write the same for you. – pushpraj Aug 25 '14 at 08:43
  • That's definitely the right direction. I think, I get the rest from here. Thanks for your patience :)! – Sebastian Graf Aug 25 '14 at 11:09
  • I am glad to be your assistance. You can always get in touch with me via http://pushpraj.com/about/say-hello, happy coding :) – pushpraj Aug 25 '14 at 13:43