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?
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;
}