I've got a question regarding animations for WPF in C# code!
I have an handler function for the MouseWheel event. It just checks if you 'zoomed in' or 'zoomed out'. Just have a look at the code, the important line here is the 4th line, where the RenderTransform is set.
private void ZoomPanCanvas_MouseWheel(object sender, MouseWheelEventArgs e) {
var factor = (e.Delta > 0) ? (1.1) : (1 / 1.1);
currrentScale = factor * currrentScale;
mNetworkUI.RenderTransform = new ScaleTransform(currrentScale, currrentScale);
var pos = e.GetPosition(mNetworkUI);
mNetworkUI.Width = ZoomPanCanvas.ActualWidth / currrentScale;
mNetworkUI.Height = ZoomPanCanvas.ActualHeight /currrentScale;
var dummyTransform = new ScaleTransform(factor, factor, pos.X, pos.Y);
var offSet = new Point(dummyTransform.Value.OffsetX, dummyTransform.Value.OffsetY);
mNetworkUI.ViewModel.Network.SetTransformOffset(offSet);
}
I kept the rest of the function in the code for completeness.
What I want to do, is to animate this change of the RenderTransform!
I already tried to use a Storyboard (with UIElement.RenderTransformProperty set). Best result was a not-animated change of the RenderTransform (but it was not the same result as this 4th line from the code achieves).
Maybe you can help me, I already tried a few suggested methods from the other questions here.
Edit:
Here's the non-working attempt and at first the chenged code from above:
private void ZoomPanCanvas_MouseWheel(object sender, MouseWheelEventArgs e) {
var factor = (e.Delta > 0) ? (1.1) : (1 / 1.1);
currrentScale = factor * currrentScale;
///mNetworkUI.RenderTransform = new ScaleTransform(currrentScale, currrentScale);
Helper.Animations.RenderTransformAnimation(mNetworkUI, new ScaleTransform(currrentScale, currrentScale));
var pos = e.GetPosition(mNetworkUI);
mNetworkUI.Width = ZoomPanCanvas.ActualWidth / currrentScale;
mNetworkUI.Height = ZoomPanCanvas.ActualHeight /currrentScale;
var dummyTransform = new ScaleTransform(factor, factor, pos.X, pos.Y);
var offSet = new Point(dummyTransform.Value.OffsetX, dummyTransform.Value.OffsetY);
mNetworkUI.ViewModel.Network.SetTransformOffset(offSet);
}
And it follows the static helper function:
public static void RenderTransformAnimation(FrameworkElement element, Transform newTransform) {
MatrixAnimationUsingKeyFrames anim = new MatrixAnimationUsingKeyFrames();
var key1 = new DiscreteMatrixKeyFrame(element.RenderTransform.Value, KeyTime.FromPercent(0));
var key2 = new DiscreteMatrixKeyFrame(newTransform.Value, KeyTime.FromPercent(1));
Storyboard.SetTarget(anim, element.RenderTransform);
Storyboard.SetTargetProperty(anim, new PropertyPath(UIElement.RenderTransformProperty));
Storyboard sb = new Storyboard();
sb.Children.Add(anim);
sb.Duration = AnimationDuration;
sb.Begin();
}
It always throws an exception on the sb.Begin() call, telling me, something was not ok with my 'PropertyPath'. I don't know how to do it :(.
I mean, there is no way to directly create a "TransformAnimation", right? Only MatrixAnimations are available...
I've provided a simple animation of a ScaleTransform below. In the interests of providing a 'minimal' example, I only adjust the scale; I don't do any of the offset calculations that you're doing based on mouse position. You should be able to figure out where to go from here:
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
var factor = e.Delta > 0d ? 1.1d : 0.9d;
var t = mNetworkUI.RenderTransform as ScaleTransform;
if (t == null)
{
mNetworkUI.RenderTransform = t = new ScaleTransform(1d, 1d)
{
CenterX = 0.5d,
CenterY = 0.5d
};
}
var oldScale = (double)t.GetAnimationBaseValue(ScaleTransform.ScaleXProperty);
var newScale = oldScale * factor;
//
// Make sure `GetAnimationBaseValue()` reflects the `To` value next time
// (needed to calculate `oldScale`, and for the animation to infer `From`).
//
t.ScaleX = newScale;
t.ScaleY = newScale;
var animation = new DoubleAnimation
{
To = newScale,
Duration = TimeSpan.FromSeconds(0.5d),
DecelerationRatio = 0.5d,
FillBehavior = FillBehavior.Stop
};
//
// Use `HandoffBehavior.Compose` to transition more smoothly if an animation
// is already in progress.
//
t.BeginAnimation(
ScaleTransform.ScaleXProperty,
animation,
HandoffBehavior.Compose);
t.BeginAnimation(
ScaleTransform.ScaleYProperty,
animation,
HandoffBehavior.Compose);
}
Related
I currently using the TranslateTransform on an image by code in my App and it's working correctly.
But I would like the image to move by step, let's say jump 10 or 20 pixel on each movement. Is there a way to do this. This to give a retro flavor to the movement.
I was thinking something like TranslateTransform.Step = 10;
Duration durationX = new TimeSpan(0, 0, 0, 0, 600);
Duration durationY = new TimeSpan(0, 0, 0, 0, 400);
DoubleAnimation moveX = new DoubleAnimation();
moveX.Duration = durationX;
moveX.To = ((ImgCanvasCoordinates[cardSource][0] - ImgCanvasCoordinates[cardTarget][0]) * -1);
DoubleAnimation moveY = new DoubleAnimation();
moveY.Duration = durationY;
moveY.To = ((ImgCanvasCoordinates[cardSource][1] - ImgCanvasCoordinates[cardTarget][1]) * -1);
Storyboard story1 = new Storyboard();
story1.Children.Add(moveX);
story1.Children.Add(moveY);
Storyboard.SetTarget(moveX, imgGhost);
Storyboard.SetTarget(moveY, imgGhost);
Storyboard.SetTargetProperty(moveX, "(Image.RenderTransform).(TranslateTransform.X)");
Storyboard.SetTargetProperty(moveY, "(Image.RenderTransform).(TranslateTransform.Y)");
story1.Begin();
You can use a custom EasingFunction to achieve this.
By writing an EasingFunction you can fully control the movement of animation. I've written a StepEasingFunction as shown below:
public class StepEasingFunction : EasingFunctionBase
{
public double NormalizedStep { get; set; }
protected override Freezable CreateInstanceCore()
{
return new StepEasingFunction {NormalizedStep = NormalizedStep};
}
protected override double EaseInCore(double normalizedTime)
{
var stepIndex = Math.Round(normalizedTime / NormalizedStep);
return NormalizedStep * stepIndex;
}
}
Then, just add an EasingFunction assigning to the animations:
moveX.EasingFunction = new StepEasingFunction {NormalizedStep = 0.2};
moveY.EasingFunction = new StepEasingFunction {NormalizedStep = 0.2};
Then, how to calculate the value of the NormalizedStep property?
You said that you want a 10-pixel movement for each step, so calculate the step total count and use 1 / stepTotalCount to calculate the final NormalizedStep property.
I want to make a turning animation to show the vehicle turning in Junction. Also, I want to do it with c#(code behind) because my vehicles are dynamically added.
Solution tried:
I tried to use TranslateTransform and RotateTransform but I could only create sharp turn animation. I want to create a smooth turn animation.
Current Output:
Sample Code
//Code to add car
private void Click1_Click(object sender, RoutedEventArgs e)
{
var myCar = new Image()
{
Source = new BitmapImage(new Uri("ms-appx:///Assets/RedCar.png")),
Width = 140,
Height = 65,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
RenderTransform = new TranslateTransform()
{
X = 0,
Y = actualHeight / 2 - 145
}
};
VehicleGrid.Children.Add(myCar);
}
//Code to create forward animation
private void MoreForward(UIElement element)
{
Storyboard storyboard = new Storyboard();
DoubleAnimation doubleAnimation = new DoubleAnimation()
{
Duration = new Duration(new TimeSpan(0, 0, 3)),
To = LeftRoad.ActualWidth - 140
};
Storyboard.SetTarget(doubleAnimation, element.RenderTransform);
Storyboard.SetTargetProperty(doubleAnimation, "X");
storyboard.Children.Add(doubleAnimation);
storyboard.Begin();
}
Full Code
You can see my full code in Github: TrafficManagementSystem
There's an interesting post that details how to create a layout path where your object can move along with.
But in a simple scenario like yours, you basically just want a curved motion like what's described in this Android's Material Design Guideline. Yeah... Android's, as we don't have native curved motion API support just yet (the Windows UI team did mention that they are looking to support this in the future though).
However, it's not too difficult to create your own curved motion. In fact, many have used this trick on the web already - apply the opposite speed of an easing on each axis. Also in your case, you will want the curve of the easing to be sharp in order to produce a nice turning animation.
For example, for a car coming from left to right and then doing a left turn, you can apply a QuinticEase with EaseIn mode on x-axis and one with EaseOut on y-axis. To turn the vehicle, just apply another rotation animation to it but with a short delay and lesser duration to ensure the turning only happens at the crossroad.
By slightly modifying my answer in this question, you can achieve what I described above with the following code
The AnimateTransform helper method
public static void AnimateTransform(this UIElement target, string propertyToAnimate, Orientation? orientation, double? from, double to, int duration = 3000, int startTime = 0, EasingFunctionBase easing = null)
{
if (easing == null)
{
easing = new ExponentialEase();
}
var transform = target.RenderTransform as CompositeTransform;
if (transform == null)
{
transform = new CompositeTransform();
target.RenderTransform = transform;
}
target.RenderTransformOrigin = new Point(0.5, 0.5);
var db = new DoubleAnimation
{
To = to,
From = from,
EasingFunction = easing,
Duration = TimeSpan.FromMilliseconds(duration)
};
Storyboard.SetTarget(db, target);
var axis = string.Empty;
if (orientation.HasValue)
{
axis = orientation.Value == Orientation.Horizontal ? "X" : "Y";
}
Storyboard.SetTargetProperty(db, $"(UIElement.RenderTransform).(CompositeTransform.{propertyToAnimate}{axis})");
var sb = new Storyboard
{
BeginTime = TimeSpan.FromMilliseconds(startTime)
};
sb.Children.Add(db);
sb.Begin();
}
Create the turning animations
MyCar.AnimateTransform("Translate", Orientation.Horizontal, null, -600, duration: 3000, easing: new QuinticEase
{
EasingMode = EasingMode.EaseIn
});
MyCar.AnimateTransform("Translate", Orientation.Vertical, null, -600, duration: 3000, easing: new QuinticEase
{
EasingMode = EasingMode.EaseOut
});
MyCar.AnimateTransform("Rotation", null, null, -90, duration: 2000, startTime: 500);
Result in motion
Alternatively, you can replace the traditional Storyboard animation with the new Composition API, which provides fully customizable easing functions (see below), but the idea is the same.
public static CubicBezierEasingFunction EaseOutExpo(this Compositor compositor) =>
compositor.CreateCubicBezierEasingFunction(new Vector2(0.14f, 1f), new Vector2(0.34f, 1f));
Hope this helps!
I have the following method which works. I'd like to put it in a utility method that returns a Storyboard. Every attempt I have made at converting this to a Storyboard has failed, and I've spent a lot of time researching. I'm ready to give up unless someone comes to my rescue.
Here's the code I want to convert:
public override void Begin(FrameworkElement element, int duration)
{
var transform = new ScaleTransform();
element.LayoutTransform = transform;
var animation = new DoubleAnimation
{
From = 1,
To = 0,
Duration = TimeSpan.FromMilliseconds(duration),
FillBehavior = FillBehavior.Stop,
EasingFunction = new QuinticEase { EasingMode = EasingMode.EaseIn }
};
transform.BeginAnimation(ScaleTransform.ScaleXProperty, animation);
transform.BeginAnimation(ScaleTransform.ScaleYProperty, animation);
}
So, instead of the two BeginAnimation() calls, I want to return a Storyboard so all I have to do is call storyboard.Begin(). I know this shouldn't be that hard to do, but I'm just not getting it.
Thanks.
EDIT: In response to H.B's suggestions, I tried the following code, which still does not work:
private static Storyboard CreateAnimationStoryboard(FrameworkElement element, int duration)
{
var sb = new Storyboard();
var scale = new ScaleTransform(1, 1);
element.RenderTransform = scale;
element.RegisterName("scale", scale);
var animation = new DoubleAnimation
{
From = 1,
To = 0,
Duration = TimeSpan.FromMilliseconds(duration),
FillBehavior = FillBehavior.Stop,
EasingFunction = new QuinticEase { EasingMode = EasingMode.EaseIn }
};
sb.Children.Add(animation);
Storyboard.SetTarget(animation, scale);
Storyboard.SetTargetProperty(animation, new PropertyPath(ScaleTransform.ScaleXProperty));
return sb;
}
I know I only animated the X axis - just want to get something to work first.
You'll need two animations and then set the attached Storyboard properties to animated the right property on the right object using SetTargetProperty and SetTargetName.
Due to how storyboards work you also need to set a namescope (NameScope.SetNameScope), register the name of the transform, and call StoryBoard.Begin with the containing element overload.
e.g.
NameScope.SetNameScope(element, new NameScope());
var transform = new ScaleTransform();
var transformName = "transform";
element.RegisterName(transformName, transform);
element.RenderTransform = transform;
var xAnimation = new DoubleAnimation(2, TimeSpan.FromSeconds(1));
var yAnimation = xAnimation.Clone();
var storyboard = new Storyboard()
{
Children = { xAnimation, yAnimation }
};
Storyboard.SetTargetProperty(xAnimation, new PropertyPath("(ScaleTransform.ScaleX)"));
Storyboard.SetTargetProperty(yAnimation, new PropertyPath("(ScaleTransform.ScaleY)"));
Storyboard.SetTargetName(xAnimation, transformName);
Storyboard.SetTargetName(yAnimation, transformName);
storyboard.Begin(element);
I suggest using Expression Blend and start recording from there, it should create your storyboards in XAML. Rather than hard coding it with C# and trying to translate it 1 by 1 to storyboard thus it can be a prone error.
There is Main frame on which there is a button and control and also stackpanel. Attempting to move worked animation from xaml to .cs I have wrote next cool function)):
private void ToggleButton_Checked(object sender, RoutedEventArgs e)
{
int heightBottom = 100;
System.Windows.Thickness th = stackPanel1.Margin;
th.Top = stackPanel1.Margin.Top + heightBottom;
ThicknessAnimation animStackPanel1 = new ThicknessAnimation
{
From = stackPanel1.Margin,
To = th,
AccelerationRatio = 0.2,
FillBehavior = FillBehavior.Stop,
DecelerationRatio = 0.8,
Duration = DURATION
};
System.Windows.Thickness th2 = fxPSEditorView.Margin;
th2.Right = fxPSEditorView.Margin.Right - heightBottom;
th2.Bottom = fxPSEditorView.Margin.Bottom - heightBottom;
ThicknessAnimation animPSEditorView = new ThicknessAnimation
{
From = fxPSEditorView.Margin,
To = th2,
AccelerationRatio = 0.2,
FillBehavior = FillBehavior.Stop,
DecelerationRatio = 0.8,
Duration = DURATION
};
Storyboard.SetTarget(animPSEditorView, fxPSEditorView);
Storyboard.SetTargetProperty(fxPSEditorView, new PropertyPath(MarginProperty));
sb.Children.Add(animPSEditorView);
Storyboard.SetTarget(animStackPanel1, stackPanel1);
Storyboard.SetTargetProperty(stackPanel1, new PropertyPath(MarginProperty));
sb.Children.Add(animStackPanel1);
sb.Begin();//null reference error here!
};
As I understand I must specify TWO parameters for sb.Begin - one for stackPanel1 and the other is for fxPSEditorView. But It does not takes a set of objects as first parameter.
Any ideas how to run this animation will be wellcome ! Thank you
You do not need two parameters, but you should pass the control which contains the controls which are being animated (which need to be in the same name-scope). Read the documentation of Begin(FrameworkElement).
I'm designing a game like this
class Anima
{
Storyboard story;
Random rand;
Canvas canvas;
Ellipse target;
public Anima() {
rand = new Random();
canvas = new Canvas();
target = new Ellipse();
target.Fill = Brushes.Red;
target.Width = 50;
target.Height = 50;
Canvas.SetLeft(target, rand.NextDouble() * 300);
Canvas.SetTop(target, rand.NextDouble() * 300);
canvas.Children.Add(target);
story = new Storyboard();
story.BeginTime = TimeSpan.FromMilliseconds(rand.Next(500, 5000));
DoubleAnimation a = new DoubleAnimation();
a.To = rand.NextDouble() * 300;
a.Duration = new Duration(TimeSpan.FromMilliseconds(50));
Storyboard.SetTarget(a, target);
Storyboard.SetTargetProperty(a, new PropertyPath(Canvas.LeftProperty));
story.Children.Add(a);
DoubleAnimation b = new DoubleAnimation();
b.To = rand.NextDouble() * 300;
b.Duration = new Duration(TimeSpan.FromMilliseconds(50));
Storyboard.SetTarget(b, target);
Storyboard.SetTargetProperty(b, new PropertyPath(Canvas.TopProperty));
story.Children.Add(b);
story.Completed += new EventHandler(story_Completed);
Window win = new Window();
win.Loaded += delegate(object sender, RoutedEventArgs e) {
story.Begin();
};
win.Content = canvas;
win.Show();
}
void story_Completed(object sender, EventArgs e) {
int next = rand.Next(500, 5000);
double left = rand.NextDouble() * 300;
double top = rand.NextDouble() * 300;
Console.WriteLine("position: ({0:G6}, {1:G6})", Canvas.GetLeft(target), Canvas.GetTop(target));
Console.WriteLine("state : wait for " + next + " ms");
Console.WriteLine("next : ({0:G6}, {1:G6})", left, top);
Console.WriteLine();
(story.Children[0] as DoubleAnimation).To = left;
(story.Children[1] as DoubleAnimation).To = top;
story.BeginTime = TimeSpan.FromMilliseconds(next);
story.Begin();
}
}
Everything is great, but I found that the ellipse didn't get the right position and make an error about 2% to 50%. It seems like the Storyboard.Completed event triggered before the end of animation.
What's wrong with it?
I would suggest that you use CompositionTarget.Rendering event or DispatcherTimer for controlling complex animations, especially if you are developing a game. The built-in animations are not precise enough and cannot be controlled easily.
Here are some links that will get you started on CompositionTarget
How to: Render on a Per Frame Interval Using CompositionTarget
Using the CompositionTarget
Fun with Animation Part 1
use Dispatcher.BeginInvoke(...), you can get the right value