Performing a Flip animation completely through code WPF - wpf

I am try to add a flip animation to a user control I built. The user control is simple it has a 87x87 image front and back and some properties. It is suppose to represent a tile in a game I am working on for fun. I am trying to animate a flipping affect of the user picking the tile from the deck. I feel I need to do this through code instead of xaml for two reasons: 1. There is another transform after the tile is flip to rotate the tile (currently working) 2. After the tile is flipped I want to unhook the event.
The issue that I am getting is only the last animation runs after the method has exited.
I think I need a Storyboard but all the examples I looked at confused me in two ways:
How do I change the image mid story board, and what do I set the targetProperty to be
I have been working off these two blogs.
http://www.codeguru.com/csharp/csharp/cs_misc/userinterface/article.php/c12221
http://blogs.msdn.com/tess/archive/2009/03/16/silverlight-wpf-flipimage-animation.aspx
public void FlipFront()
{
DoubleAnimation flipfront = new DoubleAnimation(0, 90, new Duration(new TimeSpan(0, 0, 1)));
SkewTransform skew = new SkewTransform();
this.RenderTransform = skew;
skew.BeginAnimation(SkewTransform.AngleYProperty, flipfront);
}
public void FlipBack()
{
ImageSourceConverter source = new ImageSourceConverter();
this.ImageFace.Source = new BitmapImage(new Uri("Back.jpg", UriKind.Relative));
DoubleAnimation flipfront = new DoubleAnimation(90, 0, new Duration(new TimeSpan(0, 0, 1)));
SkewTransform skew = new SkewTransform();
this.RenderTransform = skew;
skew.BeginAnimation(SkewTransform.AngleYProperty, flipfront);
}
public void Flip()
{
FlipFront();
FlipBack();
}
I broke flip into two separate methods because I though it would help fix the issue I am experiencing.

Wow, this hasn't been updated in a loong time...just in case anybody's tracking this one:
The problem is you're not waiting for the "flip front" animation to complete before immediately starting the "flip back" - now since you're basically force-jumping the Y angle animation immediately to 90 degrees, that's why it looks like it's not firing properly.
There are a LOT of ways you can work around this - the first thing that jumps to mind is that the DoubleAnimations have a method on them called CreateClock, which will return you back an AnimationClock object. That object has a Completed event on it, which will tell you when that animation is "done". Attach a handler (remember you'll want to detach it lest you leak memory), and call your "start flipping to back" method there. I've thrown something very inefficient together, but it'll show the principle:
public AnimationClock StartFlipFrontAnimation()
{
this.ImageFace.Source = _frontFace;
DoubleAnimation flipfront = new DoubleAnimation(0, 90, new Duration(new TimeSpan(0, 0, 3)));
SkewTransform skew = new SkewTransform();
this.RenderTransform = skew;
skew.BeginAnimation(SkewTransform.AngleYProperty, flipfront);
return flipfront.CreateClock();
}
public AnimationClock StartFlipBackAnimation()
{
this.ImageFace.Source = _backFace;
DoubleAnimation flipfront = new DoubleAnimation(90, 0, new Duration(new TimeSpan(0, 0, 3)));
SkewTransform skew = new SkewTransform();
this.RenderTransform = skew;
skew.BeginAnimation(SkewTransform.AngleYProperty, flipfront);
return flipfront.CreateClock();
}
public void BeginFlip()
{
var frontClk = StartFlipFrontAnimation();
frontClk.Completed += FrontFlipDone;
}
private void FrontFlipDone(object sender, EventArgs args)
{
var clk = sender as AnimationClock;
if(clk != null)
{
clk.Completed -= FrontFlipDone;
}
var backClk = StartFlipBackAnimation();
}

Related

How to achieve smooth UI updates every 16 ms?

I am trying to create sort of a radar. Radar is VisualCollection that consists of 360 DrawingVisual's (which represent radar beams). Radar is placed on Viewbox.
class Radar : FrameworkElement
{
private VisualCollection visuals;
private Beam[] beams = new Beam[BEAM_POSITIONS_AMOUNT]; // all geometry calculation goes here
public Radar()
{
visuals = new VisualCollection(this);
for (int beamIndex = 0; beamIndex < BEAM_POSITIONS_AMOUNT; beamIndex++)
{
DrawingVisual dv = new DrawingVisual();
visuals.Add(dv);
using (DrawingContext dc = dv.RenderOpen())
{
dc.DrawGeometry(Brushes.Black, null, beams[beamIndex].Geometry);
}
}
DrawingVisual line = new DrawingVisual();
visuals.Add(line);
// DISCRETES_AMOUNT is about 500
this.Width = DISCRETES_AMOUNT * 2;
this.Height = DISCRETES_AMOUNT * 2;
}
public void Draw(int beamIndex, Brush brush)
{
using (DrawingContext dc = ((DrawingVisual)visuals[beamIndex]).RenderOpen())
{
dc.DrawGeometry(brush, null, beams[beamIndex].Geometry);
}
}
protected override Visual GetVisualChild(int index)
{
return visuals[index];
}
protected override int VisualChildrenCount
{
get { return visuals.Count; }
}
}
Each DrawingVisual has precalculated geometry for DrawingContext.DrawGeometry(brush, pen, geometry). Pen is null and brush is a LinearGradientBrush with about 500 GradientStops. The brush gets updated every few milliseconds, lets say 16 ms for this example. And that is what gets laggy. Here goes the overall logic.
In MainWindow() constructor I create the radar and start a background thread:
private Radar radar;
public MainWindow()
{
InitializeComponent();
radar = new Radar();
viewbox.Child = radar;
Thread t = new Thread(new ThreadStart(Run));
t.Start();
}
In Run() method there is an infinite loop, where random brush is generated, Dispatcher.Invoke() is called and a delay for 16 ms is set:
private int beamIndex = 0;
private Random r = new Random();
private const int turnsPerMinute = 20;
private static long delay = 60 / turnsPerMinute * 1000 / (360 / 2);
private long deltaDelay = delay;
public void Run()
{
int beginTime = Environment.TickCount;
while (true)
{
GradientStopCollection gsc = new GradientStopCollection(DISCRETES_AMOUNT);
for (int i = 1; i < Settings.DISCRETES_AMOUNT + 1; i++)
{
byte color = (byte)r.Next(255);
gsc.Add(new GradientStop(Color.FromArgb(255, 0, color, 0), (double)i / (double)DISCRETES_AMOUNT));
}
LinearGradientBrush lgb = new LinearGradientBrush(gsc);
lgb.StartPoint = Beam.GradientStarts[beamIndex];
lgb.EndPoint = Beam.GradientStops[beamIndex];
lgb.Freeze();
viewbox.Dispatcher.Invoke(new Action( () =>
{
radar.Draw(beamIndex, lgb);
}));
beamIndex++;
if (beamIndex >= BEAM_POSITIONS_AMOUNT)
{
beamIndex = 0;
}
while (Environment.TickCount - beginTime < delay) { }
delay += deltaDelay;
}
}
Every Invoke() call it performs one simple thing: dc.DrawGeometry(), which redraws the beam under current beamIndex. However, sometimes it seems, like before UI updates, radar.Draw() is called few times and instead of drawing 1 beam per 16 ms, it draws 2-4 beams per 32-64 ms. And it is disturbing. I really want to achieve smooth movement. I need one beam to get drawn per exact period of time. Not this random stuff. This is the list of what I have tried so far (nothing helped):
placing radar in Canvas;
using Task, BackgroundWorker, Timer, custom Microtimer.dll and setting different Thread Priorities;
using different ways of implementing delay: Environment.TickCount, DateTime.Now.Ticks, Stopwatch.ElapsedMilliseconds;
changing LinearGradientBrush to predefined SolidColorBrush;
using BeginInvoke() instead of Invoke() and changing Dispatcher Priorities;
using InvalidateVisuals() and ugly DoEvents();
using BitmapCache, WriteableBitmap and RenderTargetBitmap (using DrawingContext.DrawImage(bitmap);
working with 360 Polygon objects instead of 360 DrawingVisuals. This way I could avoid using Invoke() method. Polygon.FillProperty of each polygon was bound to ObservableCollection, and INotifyPropertyChanged was implemented. So simple line of code {brushCollection[beamIndex] = (new created and frozen brush)} led to polygon FillProperty update and UI was getting redrawn. But still no smooth movement;
probably there were few more little workarounds I could forget about.
What I did not try:
use tools to draw 3D (Viewport) to draw 2D radar;
...
So, this is it. I am begging for help.
EDIT: These lags are not about PC resources - without delay radar can do about 5 full circles per second (moving pretty fast). Most likely it is something about multithread/UI/Dispatcher or something else that I am yet to understand.
EDIT2: Attaching an .exe file so you could see what is actually going on: https://dl.dropboxusercontent.com/u/8761356/Radar.exe
EDIT3: DispatcherTimer(DispatcherPriority.Render) did not help aswell.
For smooth WPF animations you should make use of the
CompositionTarget.Rendering event.
No need for a thread or messing with the dispatcher. The event will automatically be fired before each new frame, similar to HTML's requestAnimationFrame().
In the event update your WPF scene and you're done!
There is a complete example available on MSDN.
You can check some graphics bottleneck using the WPF Performance Suite:
http://msdn.microsoft.com/es-es/library/aa969767(v=vs.110).aspx
Perforator is the tool that will show you performance issues. Maybe you are using a low performance VGA card?
while (Environment.TickCount - beginTime < delay) { }
delay += deltaDelay;
The sequence above blocks the thread. Use instead "await Task.Delay(...)" which doesn't block the thread like its counterpart Thread.Sleep(...).

Making smooth effect in WPF manually in C# with DispatcherTimer

I'm trying to make pretty effect with not using Storyboard or another ready/already done stuff in WPF.
I want to make smooth effect, where on some event (like click) the UI element resizes for 2-3 seconds and bluring with changing color. All these items I want to make in smooth pretty way.
I have prepared such class to render each frame of my effect:
public static class ApplicationHelper
{
[SecurityPermissionAttribute(SecurityAction.Demand,
Flags=SecurityPermissionFlag.UnmanagedCode)]
public static void DoEvents(DispatcherPriority priority)
{
DispatcherFrame frame = new DispatcherFrame();
DispatcherOperation oper = Dispatcher.CurrentDispatcher.
BeginInvoke(priority,
new DispatcherOperationCallback(ExitFrameOperation),
frame);
Dispatcher.PushFrame(frame);
if (oper.Status != DispatcherOperationStatus.Completed)
{
oper.Abort();
}
}
private static object ExitFrameOperation(object obj)
{
((DispatcherFrame)obj).Continue = false;
return null;
}
[SecurityPermissionAttribute(SecurityAction.Demand,
Flags=SecurityPermissionFlag.UnmanagedCode)]
public static void DoEvents()
{
DoEvents(DispatcherPriority.Background);
}
}
Here I'm trying to make it work with DispatcherTimer:
void vb1_click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
DispatcherTimer dt = new DispatcherTimer();
dt.Interval = new TimeSpan(0, 0, 0, 0, 500);
dt.Tick += new System.EventHandler(dt_Tick);
dt.Start();
}
void dt_Tick(object sender, System.EventArgs e)
{
for(int i = 0; i < 20; i++)
{
this.vb2_blur_eff.Radius = (double)i;
ApplicationHelper.DoEvents();
}
}
The main problem is, that when I'm launcing it, I'm only waiting and at the final time ( when must last frame be rendered ) , I'm getting in a very quick speed all frames, but perviously there was nothing.
How to solve it and make perfect smooth effect in pure C# way with not using some ready/done stuff?
Thank you!
The ApplicationHelper.DoEvents() in dt_Tick probably does nothing, since there are no events to process. At least not the ones you're probably expecting.
If I'm not mistaken, your code will just quickly set the Radius to 0, then 1, 2, and so on in quick succession, and finally to 19. All of that will happen every 500 milliseconds (on every Tick, that is).
I think you might believe that each Tick will only set Radius to one value and then wait for the next Tick, but it does not. Every Tick will set the Radius to all the values, ending at 19. That is one possible explanation for what you're experiencing.
I would also like to comment on the DoEvents approach. It's most likely a bad idea. Whenever I see a DoEvents I get chills up my spine. (It reminds me of some seriously bad Visual Basic 5/6 code I stumbled across 10-15 years ago.) As I see it, an event handler should return control of the GUI thread as quickly as possible. If the operation takes a not insignificant amount of time, then you should delegate that work to a worker thread. And nowadays, you have plenty of options for writing asynchronous code.

WPF TranslateTransform Error

I have a StackPanel that I am sliding left and right to simulate moving pages using TranslateTransform.
If I call the slide method once it works well. If I call my slide method twice in quick succession (in code), the second transform has the wrong start position and ends up in the wrong place.
How do I get my second translate to "refresh" its starting position?
Here is the StackPanel
<StackPanel
x:Name="MainPanel"
Orientation="Horizontal">
<StackPanel.RenderTransform>
<TranslateTransform
x:Name="MainPanelTransform"
/>
</StackPanel.RenderTransform>
</StackPanel>
Here is the slide code:
private void SlidePage(int pagesToMove)
{
Storyboard sb = SlideEffect(MainPanel, (-PageWidth * pagesToMove));
sb.Completed += SlideCompleted;
sb.Begin();
}
private Storyboard SlideEffect(UIElement controlToAnimate, double positionToMove)
{
//Get position of stackpanel
var gt = controlToAnimate.TransformToVisual(MainGrid);
var p = gt.Transform(new Point(0, 0));
//add new storyboard and animation
var sb = new Storyboard();
var da = new DoubleAnimation { To = p.X + positionToMove };
Storyboard.SetTarget(da, controlToAnimate);
Storyboard.SetTargetProperty(da, new PropertyPath("RenderTransform.(TranslateTransform.X)"));
//Storyboard.SetTargetProperty(da, new PropertyPath("RenderTransform.Children[0].X"));
var ee = new ExponentialEase { Exponent = 6.0, EasingMode = EasingMode.EaseOut };
da.EasingFunction = ee;
sb.Children.Add(da);
return sb;
}
The offending line of code is this:
var p = gt.Transform(new Point(0, 0));
The second time I call the SlideEffect method it has the same value as the first time. It seems like the animations are getting buffered and run together. Is there any way to stop the buffering?
Did you try to change a FillBehavior property of Storyboard?
UPDATE:
I have a bug in VS, so I couldn't reproduce your situation. But I may recommend you:
Try to disable the EasingFunction. Maybe Storyboard.Completed fires before the Storyboard actually completed;
Are you sure, that transform changes actual coordinates? I think it is the solution, but not sure.
In the Storyboard.Completed you could manually update transform coordinates:
MainPanelTransform.BeginAnimation(TranslateTransform.XProperty, null);
MainPanelTransform.X = -100;

WPF DrawingContext seems ignore SnapToDevicePixels

I'm drawing a chart by direct calls to DrawLine on the DrawingContext. Since I want to avoid any anti aliasing feature, I tryed to put the SnapToDevicePixels=true on the parent UIElement, but I still have anti-alias:
The project was an old OS project not written for WPF4, but I retarget it to the Framework4, can this be an issue too ?
I found this link where they basically say you should set
ParentUIElement.SetValue(RenderOptions.EdgeModeProperty, EdgeMode.Aliased);
It worked for me, so it might be worth a try!
SnapsToDevicePixels works only for element bounding box. You need to use Guidelines with DrawingContext. Also you can specify VisualXSnappingGuidelines and VisualYSnappingGuidelines if it fits to your requirements.
GuidelineSet is designed to resolve your problems.
#Marat Khasanov recommended you to use GuidelineSet and you replied that it messed up your code. I'm also suffering from this problem, so I write the code below to solve this problem with non-ugly code.
Notice: This method works even in a Viewbox.
public static class SnapDrawingExtensions
{
public static void DrawSnappedLinesBetweenPoints(this DrawingContext dc,
Pen pen, double lineThickness, params Point[] points)
{
var guidelineSet = new GuidelineSet();
foreach (var point in points)
{
guidelineSet.GuidelinesX.Add(point.X);
guidelineSet.GuidelinesY.Add(point.Y);
}
var half = lineThickness / 2;
points = points.Select(p => new Point(p.X + half, p.Y + half)).ToArray();
dc.PushGuidelineSet(guidelineSet);
for (var i = 0; i < points.Length - 1; i = i + 2)
{
dc.DrawLine(pen, points[i], points[i + 1]);
}
dc.Pop();
}
}
Call the method in OnRender and pass line points to it.
protected override void OnRender(DrawingContext dc)
{
// Draw four horizontal lines and one vertical line.
// Notice that even the point X or Y is not an integer, the line is still snapped to device.
dc.DrawSnappedLinesBetweenPoints(_pen, LineThickness,
new Point(0, 0), new Point(320, 0),
new Point(0, 40), new Point(320, 40),
new Point(0, 80.5), new Point(320, 80.5),
new Point(0, 119.7777), new Point(320, 119.7777),
new Point(0, 0), new Point(0, 120));
}
Here is the render result in a viewbox:

Why don't these animations work when I'm using a storyboard?

I've created a simple subclass of StackPanel that I can move around on the screen using an animated TranslateTransform. It looks like this:
public class MovingStackPanel : StackPanel
{
public void BeginMove(Point translatePosition)
{
RenderTransform = new TranslateTransform();
Duration d = new Duration(new TimeSpan(0, 0, 0, 0, 400));
DoubleAnimation x = new DoubleAnimation(translatePosition.X, d);
DoubleAnimation y = new DoubleAnimation(translatePosition.Y, d);
/*
Storyboard.SetTarget(x, RenderTransform);
Storyboard.SetTargetProperty(x, new PropertyPath("X"));
Storyboard.SetTarget(y, RenderTransform);
Storyboard.SetTargetProperty(y, new PropertyPath("Y"));
Storyboard sb = new Storyboard();
sb.Children.Add(x);
sb.Children.Add(y);
sb.Completed += sb_Completed;
sb.Begin();
*/
RenderTransform.BeginAnimation(TranslateTransform.XProperty, x);
RenderTransform.BeginAnimation(TranslateTransform.YProperty, y);
}
void sb_Completed(object sender, EventArgs e)
{
Console.WriteLine("Completed.");
}
}
And here is my problem: If I animate the X and Y properties directly, as the code above does, it works. But if I use the commented-out code above it, which is really the simplest creation of a Storyboard in code imaginable, nothing happens. The animation runs - at least, the Completed event gets raised - but nothing changes on the screen.
Clearly I'm doing something wrong, but I can't see what it is. Every example of creating storyboards in code I've seen looks just like this. There's obviously something about animations and storyboards that I don't know yet: what is it?
As it turns out, you can't use property path syntax in this case, because the properties being animated aren't properties of a FrameworkElement. At least, that's how I interpret the remarkably bewildering exception that I get when I make the change that Anvaka suggested:
Cannot automatically create animation clone for frozen property values on
'System.Windows.Media.TranslateTransform' objects. Only FrameworkElement and
FrameworkContentElement (or derived) types are supported.
To animate those, it seems, I have to use a NameScope and use SetTargetName to name the TransformElement. Then, as long as I pass the FrameworkElement that I set the name scope on to the Begin method, the storyboard can find the object and the properties and animate them and it all works. The end result looks like this:
public void BeginMove(Point translatePosition)
{
NameScope.SetNameScope(this, new NameScope());
RenderTransform = new TranslateTransform();
RegisterName("TranslateTransform", RenderTransform);
Duration d = new Duration(new TimeSpan(0, 0, 0, 0, 400));
DoubleAnimation x = new DoubleAnimation(translatePosition.X, d);
DoubleAnimation y = new DoubleAnimation(translatePosition.Y, d);
Storyboard.SetTargetName(x, "TranslateTransform");
Storyboard.SetTargetProperty(x, new PropertyPath(TranslateTransform.XProperty));
Storyboard.SetTargetName(y, "TranslateTransform");
Storyboard.SetTargetProperty(y, new PropertyPath(TranslateTransform.YProperty));
Storyboard sb = new Storyboard();
sb.Children.Add(x);
sb.Children.Add(y);
sb.Completed += sb_Completed;
// you must pass this to the Begin method, otherwise the timeline won't be
// able to find the named objects it's animating because it doesn't know
// what name scope to look in
sb.Begin(this);
}
It's property path syntax. The following approach works:
public void BeginMove(Point translatePosition)
{
RenderTransform = new TranslateTransform();
Duration d = new Duration(new TimeSpan(0, 0, 0, 0, 400));
DoubleAnimation x = new DoubleAnimation(translatePosition.X, d);
DoubleAnimation y = new DoubleAnimation(translatePosition.Y, d);
Storyboard.SetTarget(x, this);
Storyboard.SetTargetProperty(x,
new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.X)"));
Storyboard.SetTarget(y, this);
Storyboard.SetTargetProperty(y,
new PropertyPath("(UIElement.RenderTransform).(TranslateTransform.Y)"));
Storyboard sb = new Storyboard();
sb.Children.Add(x);
sb.Children.Add(y);
sb.Completed += sb_Completed;
sb.Begin();
//RenderTransform.BeginAnimation(TranslateTransform.XProperty, x);
//RenderTransform.BeginAnimation(TranslateTransform.YProperty, y);
}

Resources