How to animate using Path with Storyboard in C# - c#

So, I am trying to move my rectangular boxes around a grid like this -
For this, I am using a Story board.
I am using DoubleAnimation to move the boxes on X-Axis and Y-Axis in one of my Class. I am calling this class from MainWindow class. But, for every box, and for every turn, I have to create a new Double animation, assign the offset values, the start time, the duration etc. like this -
//Code to move Boxes 1-4 to first grid point in their path
TranslateTransform moveTransform = new TranslateTransform();
moveTransform.X = 0;
moveTransform.Y = 0;
x.RenderTransform = moveTransform;
Storyboard s = new Storyboard();
DoubleAnimation Move1= new DoubleAnimation();
Move1.From = 0;
Move1.To = xPosition; // calculate correct offset here
Move1.Duration = new Duration(TimeSpan.FromSeconds(hops));
if (x==Box2)
{
Move1.BeginTime = TimeSpan.FromSeconds(5);//For Box 2, the first move will be across Y-Axis and hence X-Axis move will be delayed by 5 seconds.
}
else
{
Move1.BeginTime = TimeSpan.Zero;
}
Storyboard.SetTarget(Move1, x);
Storyboard.SetTargetProperty(Move1, new PropertyPath("(UIElement.RenderTransform).(X)"));
s.Children.Add(Move1);
I know there is a way to define the path to reach the destination from source, but I am not sure how to do it? Also, I am not sure if what I am doing here is the optimal way.
So, my question is -
What is the better way to do this? How can we define paths for animation?
I am a newbie to C# so please do not mind if this sounds silly.
Thank you!

I was able to do it without using path.
For every move, I created a new double animation.
So for Left - Down - Right movement, I created 3 double animations with
X-Axis, Y-Axis and X-Axis again.

Related

How to move a WPF shape using an animation

I want to move a shape using an animation and am currently using the following code. However this does not result in the object actually moving, it just seems to change the location the object is rendered in, which is expected given we are setting the crossHair.RenterTransform.
EDIT:
To clarify - I am using the animation to simulate what an input file instructions contain and as a result think I can only build the animations in code when parsing the input files. I could be wrong about this but can't see how one could do this in XAML. The input file format is not in XAML.
Because there are a number of sequential moves contained in the input file I an currently using the shapes current position as the start point for the next animation, however this does not work because it seems the animation is not actually moving the shape.
As a work around I am now changing the shapes actual position in the animations completion handler. This seems to be working.
So the question remains as to how can I use the same transform to actually move the shape rather than simply rendering it in a different position ?
private Storyboard MoveCrossHairToPoint(double x, double y)
{
// Adjust for crosshair size so its centered on the x
double xPos = x;
double yPos = y;
double xStart = Canvas.GetLeft(crossHair)+crossHair.Width/2;
double yStart = Canvas.GetTop(crossHair)+crossHair.Height/2;
// Create a NameScope for the page so that 
// we can use Storyboards.
NameScope.SetNameScope(this, new NameScope());
// Create a MatrixTransform. This transform 
// will be used to move the crossHair.
MatrixTransform crossHairMatrixTransform = new MatrixTransform();
crossHair.RenderTransform = crossHairMatrixTransform;
// Register the transform's name with the page 
// so that it can be targeted by a Storyboard. 
this.RegisterName("MoveCrossHairMatrixTransform", crossHairMatrixTransform);
// Create the animation path.
PathGeometry animationPath = new PathGeometry();
PathFigure pFigure = new PathFigure();
pFigure.StartPoint = new Point(xStart, yStart);
LineSegment lineSegment = new LineSegment(new Point(x, y),true);
pFigure.Segments.Add(lineSegment);
animationPath.Figures.Add(pFigure);
// Create a path to follow
Path path = new Path();
path.Data = animationPath;
path.Stroke = System.Windows.Media.Brushes.Green;
this.bedCanvas.Children.Add(path);
// Freeze the PathGeometry for performance benefits.
animationPath.Freeze();
// Create a MatrixAnimationUsingPath to move the 
// button along the path by animating 
// its MatrixTransform.
MatrixAnimationUsingPath matrixAnimation =
new MatrixAnimationUsingPath();
matrixAnimation.PathGeometry = animationPath;
double time = GetTimeForVelocityOverPath(animationPath, this.velocityMove);
matrixAnimation.Duration = TimeSpan.FromSeconds(time);
//matrixAnimation.RepeatBehavior = RepeatBehavior.;
// Set the animation's DoesRotateWithTangent property 
// to true so that rotates the rectangle in addition 
// to moving it.
matrixAnimation.DoesRotateWithTangent = false;
// Set the animation to target the Matrix property 
// of the MatrixTransform named "ButtonMatrixTransform".
Storyboard.SetTargetName(matrixAnimation, "MoveCrossHairMatrixTransform");
Storyboard.SetTargetProperty(matrixAnimation,
new PropertyPath(MatrixTransform.MatrixProperty));
// Create a Storyboard to contain and apply the animation.
Storyboard pathAnimationStoryboard = new Storyboard();
pathAnimationStoryboard.Children.Add(matrixAnimation);
return pathAnimationStoryboard;
}

slider value to change the duration of spin using DoubleAnimation class

I am trying to do a basic animation using the DoubleAnimation class in WPF. On a button click the rectangle spins. The cs code is
DoubleAnimation da = new DoubleAnimation();
da.From = 0; // start from 0 to 360, full circle
da.To = 360;
da.Duration = new Duration(TimeSpan.FromSeconds(0.5));
da.RepeatBehavior = RepeatBehavior.Forever;
RotateTransform rt = new RotateTransform();
rectangle1.RenderTransform = rt;
rt.BeginAnimation(RotateTransform.AngleProperty, da);
What i am trying to do is to update the da.Duration = new Duration(TimeSpan.FromSeconds(0.5)) value with the slider value.
Tried with slider value binding, but not able to get this correctly.
Changing the Duration is not enough. How fast the animation is controlled by the inner AnimationClock. This clock is initialized just once after you call BeginAnimation(). So you have to update the inner AnimationClock by using ApplyAnimationClock() method.
Also note that you should also set the From property to the currently animated value, so that when changing the Slider's Value, the Angle will be animated smoothly (without restarting from 0):
//The ValueChanged handler for your Slider
private void slider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs e){
da.Duration = new Duration(TimeSpan.FromMilliseconds(yourSlider.Value));
da.From = rt.Angle;
rt.ApplyAnimationClock(RotateTransform.AngleProperty, da.CreateClock());
}
Note that I suppose the slider's Value corresponds to the number of milliseconds, so the Maximum value of the Slider should be large enough (about several thousands) if you don't want your Rectangle to be rotated super fast. Also all the variables you declared should be able to access inside the ValueChanged handler.
One more important note is you should use By property here instead of To, like this:
da.By = 360;
That way you can just update the From without caring about the To (it is always 360 degrees rotated).

WPF TranslateTransform

Im trying to use the TranslateTransform class to move a image on a Grid on Y axis. I need this movment to be smooth so i can not use SetMargin or SetCanvas. I try this in code behind:
public void MoveTo(Image target, double oldY, double newY)
{
var trans = new TranslateTransform();
var anim2 = new DoubleAnimation(0, newY, TimeSpan.FromSeconds(2))
{EasingFunction = new SineEase()};
target.RenderTransform = trans;
trans.BeginAnimation(TranslateTransform.YProperty, anim2);
}
The object i want to use (a Image control) is placed on a Grid.
For the first time everything works fine.
The problems comes when i try to move the object again using the same function.
The object (a Image control) first move to the start position (initial Y coordinate) then the animation begins.
Is it not suposed for the TranslateTransform to change the coordinates (in my case the Margin property) too?
Thank you.
The transform does not change the original values.they are your point of origin. If you want a new point of origin each time you move you can handle the animation completed event. Or from your transform you can get your current offset and make that your new start point for the animation.
In other words your start values would always be your last move to values
The TranslateTransform is a specific kind of render transformation. Rather that changing properties of the control (such as the Margin property), it simply affects how the control is displayed on the screen.
You have to use the By property of DoubleAnimation.
Try that:
//everytime you execute this anmation your object will be moved 2.0 further
double offset = 2.0
var anim2 = new DoubleAnimation(newY, TimeSpan.FromSeconds(2));
anim2.To = null;
anim2.By = offset;
You've explicitly told the animation to start from 0. It's doing what you've told it.
Just remove the explicit zero fromvalue and everything will work.
var anim2 = new DoubleAnimation(newY, TimeSpan.FromSeconds(2))
{ EasingFunction = new SineEase() };

How can I simulate a hanging cable in WPF?

I have an application that is very "connection-based", i.e. multiple inputs/outputs.
The UI concept of a "cable" is exactly what I'm looking for to make the concept clear to the user. Propellerhead took a similar approach in their Reason software for audio components, illustrated in this YouTube video (fast forward to 2m:50s).
I can make this concept work in GDI by painting a spline from point A to point B, there's got to be a more elegant way to use Paths or something in WPF for this, but where do you start? Is there a good way to simulate the animation of the cable swing when you grab it and shake it?
I'm also open to control libraries (commercial or open source) if this wheel has already been invented for WPF.
Update: Thanks to the links in the answers so far, I'm almost there.
I've created a BezierCurve programmatically, with Point 1 being (0, 0), Point 2 being the bottom "hang" point, and Point 3 being wherever the mouse cursor is. I've created a PointAnimation for Point 2 with an ElasticEase easing function applied to it to give the "Swinging" effect (i.e., bouncing the middle point around a bit).
Only problem is, the animation seems to run a little late. I'm starting the Storyboard each time the mouse moves, is there a better way to do this animation? My solution so far is located here:
Bezier Curve Playground
Code:
private Path _path = null;
private BezierSegment _bs = null;
private PathFigure _pFigure = null;
private Storyboard _sb = null;
private PointAnimation _paPoint2 = null;
ElasticEase _eEase = null;
private void cvCanvas_MouseMove(object sender, MouseEventArgs e)
{
var position = e.GetPosition(cvCanvas);
AdjustPath(position.X, position.Y);
}
// basic idea: when mouse moves, call AdjustPath and draw line from (0,0) to mouse position with a "hang" in the middle
private void AdjustPath(double x, double y)
{
if (_path == null)
{
_path = new Path();
_path.Stroke = new SolidColorBrush(Colors.Blue);
_path.StrokeThickness = 2;
cvCanvas.Children.Add(_path);
_bs = new BezierSegment(new Point(0, 0), new Point(0, 0), new Point(0, 0), true);
PathSegmentCollection psCollection = new PathSegmentCollection();
psCollection.Add(_bs);
_pFigure = new PathFigure();
_pFigure.Segments = psCollection;
_pFigure.StartPoint = new Point(0, 0);
PathFigureCollection pfCollection = new PathFigureCollection();
pfCollection.Add(_pFigure);
PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures = pfCollection;
_path.Data = pathGeometry;
}
double bottomOfCurveX = ((x / 2));
double bottomOfCurveY = (y + (x * 1.25));
_bs.Point3 = new Point(x, y);
if (_sb == null)
{
_paPoint2 = new PointAnimation();
_paPoint2.From = _bs.Point2;
_paPoint2.To = new Point(bottomOfCurveX, bottomOfCurveY);
_paPoint2.Duration = new Duration(TimeSpan.FromMilliseconds(1000));
_eEase = new ElasticEase();
_paPoint2.EasingFunction = _eEase;
_sb = new Storyboard();
Storyboard.SetTarget(_paPoint2, _path);
Storyboard.SetTargetProperty(_paPoint2, new PropertyPath("Data.Figures[0].Segments[0].Point2"));
_sb.Children.Add(_paPoint2);
_sb.Begin(this);
}
_paPoint2.From = _bs.Point2;
_paPoint2.To = new Point(bottomOfCurveX, bottomOfCurveY);
_sb.Begin(this);
}
If you want true dynamic motion (ie, when you "shake" the mouse pointer you can create waves that travel along the cord), you will need to use finite element techniques. However if you are ok with static behavior you can simply use Bezier curves.
First I'll briefly describe the finite element approach, then go into more detail on the static approach.
Dynamic approach
Divide your "cord" into a large number (1000 or so) "elements", each with a position and velocity Vector. Use the CompositionTarget.Rendering event to compute each element position as follows:
Compute the pull on each element along the "cord" from adjacent elements, which is proportional to the distance between elements. Assume the cord itself is massless.
Compute the net force vector on each "element" which consists of the pull from each adjacent element along the cord, plus the constant force of gravity.
Use a mass constant to convert the force vector to accelaration, and update the position and velocity using the equations of motion.
Draw the line using a StreamGeometry build with a BeginFigure followed by a PolyLineTo. With so many points there is little reason to do the extra computations to create a cubic bezier approximation.
Static approach
Divide your cord into perhaps 30 segments, each of which is a cubic bezier approximation to the catenary y = a cosh(x/a). Your end control points should be on the catenary curve, the parallels should tangent to the catenaries, and the control line lengths set based on the second derivative of the catenary.
In this case you will probably also want to render a StreamGeometry, using BeginFigure and PolyBezierTo to build it.
I would implement this as a custom Shape subclass "Catenary" similar to Rectangle and Ellipse. In that case, all you have to override the DefiningGeometry property. For efficiency I would also override CacheDefiningGeometry, GetDefiningGeometryBounds, and GetNaturalSize.
You would first decide how to parameterize your catenary, then add DependencyProperties for all your parameters. Make sure you set the AffectsMeasure and AffectsRender flags in your FrameworkPropertyMetadata.
One possible parameterization would be XOffset, YOffset, Length. Another might be XOffset, YOffset, SagRelativeToWidth. It would depend on what would be easiest to bind to.
Once your DependencyProperties are defined, implement your DefiningGeometry property to compute the cubic bezier control points, construct the StreamGeometry, and return it.
If you do this, you can drop a Catenary control anywhere and get a catenary curve.
User bezier curve segments in a path.
http://www.c-sharpcorner.com/UploadFile/dbeniwal321/WPFBezier01302009015211AM/WPFBezier.aspx
IMHO 'hanging' (physically simulated) cables are a case of over-doing it - favouring looks over usability.
Are you sure you're not just cluttering the user-experience ?
In a node/connection-based UI I find clear connections (like in Quartz Composer : http://ellington.tvu.ac.uk/ma/wp-content/uploads/2006/05/images/Quartz%20Composer_screenshot_011.png ) way more important than eye-candy like swinging cables that head in a different direction (down due to gravity) than where the actually connection-point is. (And in the mean time eat up CPU-cycles for the simulation that could be more useful elsewhere)
Just my $0.02

WPF Animation - Animating Bezier curve points

I'm working on a project that involves drawing curved paths between two objects. Currently, I've been writing some test code to play around with bezier curves and animation. The first test is simply to move the endpoint (Point3) from the origin object (a rectangle) to the destination object (another rectangle), in a straight line. here is the code which sets up the actual line:
connector = new Path();
connector.Stroke = Brushes.Red;
connector.StrokeThickness = 3;
PathGeometry connectorGeometry = new PathGeometry();
PathFigure connectorPoints = new PathFigure();
connectorCurve = new BezierSegment();
connectorPoints.StartPoint = new Point((double)_rect1.GetValue(Canvas.LeftProperty) + _rect1.Width / 2,
(double)_rect1.GetValue(Canvas.TopProperty) + _rect1.Height / 2);
connectorCurve.Point1 = connectorPoints.StartPoint;
connectorCurve.Point2 = connectorPoints.StartPoint;
connectorCurve.Point3 = connectorPoints.StartPoint;
connectorPoints.Segments.Add(connectorCurve);
connectorGeometry.Figures.Add(connectorPoints);
connector.Data = connectorGeometry;
MainCanvas.Children.Add(connector);
OK, so we now have a line collapsed to a point. Now, lets animate that line, going from _rect1 to _rect2 (the two objects at the endpoints):
PointAnimation pointAnim = new PointAnimation();
pointAnim.From = connectorCurve.Point3;
pointAnim.To = new Point((double)_rect2.GetValue(Canvas.LeftProperty) + _rect2.Width / 2,
(double)_rect2.GetValue(Canvas.TopProperty) + _rect2.Height / 2);
pointAnim.Duration = new Duration(TimeSpan.FromSeconds(5));
board.Children.Add(pointAnim);
Works beautifully. However, when I try to do it with a storyboard, I get nothing. Here's the storyboarded code:
Storyboard board = new Storyboard();
PointAnimation pointAnim = new PointAnimation();
pointAnim.From = connectorCurve.Point3;
pointAnim.To = new Point((double)_rect2.GetValue(Canvas.LeftProperty) + _rect2.Width / 2,
(double)_rect2.GetValue(Canvas.TopProperty) + _rect2.Height / 2);
pointAnim.Duration = new Duration(TimeSpan.FromSeconds(5));
Storyboard.SetTarget(pointAnim, connectorCurve);
Storyboard.SetTargetProperty(pointAnim, new PropertyPath(BezierSegment.Point3Property));
board.Children.Add(pointAnim);
board.Begin();
Nothing moves. I'm suspecting there is a problem with what I'm feeding SetTarget or SetTargetProperty, but can't seem to figure it out. Does anyone have experience with animating line / bezier points in WPF?
I recreated your code, and this works:
Storyboard.SetTarget(pointAnim, connector);
Storyboard.SetTargetProperty(pointAnim, new PropertyPath("Data.Figures[0].Segments[0].Point3"));
That fixes it :) It seems that the target needs to be the control itself.
Going one step down, like this:
Storyboard.SetTarget(pointAnim, connectorGeometry);
Storyboard.SetTargetProperty(pointAnim, new PropertyPath("Figures[0].Segments[0].Point3"));
...gives the InvalidOperationException:
'[Unknown]' property value in the path 'Figures[0].Segments[0].Point3' points to immutable instance of 'System.Windows.Media.PathFigure'.
http://msdn.microsoft.com/en-us/library/system.windows.media.animation.storyboard(VS.95).aspx says:
Do not attempt to call Storyboard members (for example, Begin) within the constructor of the page. This will cause the animation to fail silently.
..in case you were doing that!
The sample on that page also sets the Duration property of the Storyboard object.
Finally a general tip, with these kinds of UI objects and weird XAML object graphs once you've got the basics working best to put it in a ResourceDictionary and use something like 'Resources["Name"] as Storyboard' to get it back later.
Hope that's helpful: looks like the missing Duration should do the trick.
edit: Looks like Duration is set to Automatic by default, I will see what else I can come up with, please bear with me.. :)

Categories

Resources