Search code examples
wpfxamlstoryboardwpf-stylewpf-animation

Update Storyboard Binding in Real TIme


I want to animate an object along a circle with a sinusoidal radius, where the amplitude and frequency can change.

I've managed to create the sinusoidal circle, and an object which follows its initial path. Additionally, as I change the amplitude of the circle it does change. However, the object's path does not update and I'm not sure how to make it do so.

The XAML:

<Grid>
    <Path HorizontalAlignment="Center"
          VerticalAlignment="Center"
          Stretch="Uniform" x:Name="MainPath" SizeChanged="MainPath_SizeChanged" Fill="Green" Stroke="Black">
        <Path.Data>
            <PathGeometry x:Name="ScaledAnimationPath">
                <PathGeometry.Figures>
                <PathFigure StartPoint="{Binding StartPoint}">
                        <PolyLineSegment Points="{Binding Points}"/>
                    </PathFigure>
                </PathGeometry.Figures>
            </PathGeometry>
        </Path.Data>
    </Path>
<Ellipse Width="30" Height="30" Fill="Blue">
    <Ellipse.RenderTransform>
        <TranslateTransform x:Name="AnimatedTranslateTransform"  />
    </Ellipse.RenderTransform>

    <Ellipse.Triggers>
        <EventTrigger RoutedEvent="Path.Loaded">
            <BeginStoryboard>
                <Storyboard RepeatBehavior="Forever">

                    <!-- Animates the rectangle horizotally along the path. -->
                    <DoubleAnimationUsingPath
        Storyboard.TargetName="AnimatedTranslateTransform"
        Storyboard.TargetProperty="X"
        PathGeometry="{Binding ElementName=ScaledAnimationPath}"
        Source="X" 
        Duration="0:0:3"  />

                    <!-- Animates the rectangle vertically along the path. -->
                    <DoubleAnimationUsingPath
        Storyboard.TargetName="AnimatedTranslateTransform"
        Storyboard.TargetProperty="Y"
        PathGeometry="{Binding ElementName=ScaledAnimationPath}"
        Source="Y"
        Duration="0:0:3"  />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Ellipse.Triggers>
</Ellipse>

(the SizeChanged event calls a method in the VM - done to scale the PolyLine's points with the size of the element)

The ViewModel:

public class ParticlePathVM : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    public void RaisePropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }


    PointCollection points;
    public PointCollection Points { get => points; set { if (points != value) { points = value; RaisePropertyChanged(nameof(Points)); } } }

    Point startPoint;
    public Point StartPoint { get => startPoint; set { if (startPoint != value) { startPoint = value; RaisePropertyChanged(nameof(StartPoint)); } } }

    double amplitude;
    public double Amplitude { get => amplitude; set { if (amplitude != value) { amplitude = value; RaisePropertyChanged(nameof(Amplitude)); UpdatePoints(); } } }

    public double AmplitudeMax { get; } = 10;

    public double Radius;
    public double Frequency;
    System.Threading.SynchronizationContext _context;

    public ParticlePathVM()
    {
        _context = System.Threading.SynchronizationContext.Current;

        Radius = 266 - AmplitudeMax;
        StartPoint = new Point(Radius, 0);
        Amplitude = 5;
        Frequency = 10;
        UpdatePoints();
    }

    public void UpdateBounds(Size s)
    {
        if (s.Width > 1)
        {
            Radius = (s.Width / 2) - AmplitudeMax;
            Debug.WriteLine(string.Format("{0} -> {1}", s, Radius));
            StartPoint = new Point(Radius, 0);
            UpdatePoints();
        }
    }

    void UpdatePoints()
    {
        var points = new List<Point>();
        for (float i = 0; i <= 360; i += 0.5F)
        {
            points.Add(GetCartesian(i));
        }
        _context.Post(o =>
        {
            Points = new PointCollection(points);
        }, null);

    }

    public Point GetCartesian(double angle)
    {
        var rad = angle * (Math.PI / 180.0);
        var r = Radius + Amplitude * Math.Sin(2 * Frequency * rad);
        return new Point(r * Math.Cos(rad), r * Math.Sin(rad));
    }
}

Again - when I change the amplitude from outside the VM, the shape of the circle does change, but the moving object does not change its path. It seems I need a way to update the storyboard and I'm not sure how to do that. Any suggestions?


Solution

  • I tried the DynamicResource as suggested in the comments, but that didn't immediately work.

    Since I'm defining the path in the code behind, I was able to simply bind the position of the TranslateTransform on my object to the VM and use the code representation of the path to move the object.

        <Ellipse Width="30" Height="30" Fill="Blue">
            <Ellipse.RenderTransform>
                <TranslateTransform X="{Binding PointX}" Y="{Binding PointY}"/>
            </Ellipse.RenderTransform>
        </Ellipse>
    

    And then

        double pointX;
        public double PointX { get => pointX; set { if (pointX != value) { pointX = value; RaisePropertyChanged(nameof(PointX)); } } }
    
        double pointY;
        public double PointY { get => pointY; set { if (pointY != value) { pointY = value; RaisePropertyChanged(nameof(PointY)); } } }
        double pointSpeed = 0.1;
        double pointAngle = 0;
    
        public void UpdatePoint(double ms)
        {
            pointAngle += (pointSpeed * (ms/1000));
            while(pointAngle > 360)
            {
                pointAngle -= 360;
            }
            var p = GetCartesian(pointAngle);
            PointX = p.X;
            PointY = p.Y;
        }