## Animating with easings

Discussion related to the Cross Codebot open source framework
sysrpl
Posts: 108
Joined: Thu Feb 05, 2015 6:31 pm

### Animating with easings

This article describes how to animate cross platform graphics using easings. This example below which demonstrates animation using easings is included in the Cross Codebot git repository.

In previous forum postings I supplied a background for the basics of fast hardware accelerated drawing using the ISurface interface. Continuing on from those topics, this topic describes how to add motion to applications.

Motion is simply changes to drawings over time. The changes don't have to be complicated to create a nice effect. They can be something as simple as animating the opacity on a button when it is pressed or released. Or they can be complex involving animating vector graphics. In either case, we call motion applied to graphics animation.

A fundamental concept in animation is motion interpolation. Motion interpolation is the filling in of motion values between two values. If a button were to animate from 50% opacity to 100% opacity in one second, you can simply fill in the differing levels of transparency like so (time is a value between 0.0 seconds and 1.0 seconds):

opacity = 0.5 * (1 - time) + 1.0 * time

And this is a linear interpolation between 0.5 and 1.0. As time reaches 1.0 seconds the opacity comes closer to 1.0. As time is nearer to 0.0 seconds opacity is comes close to 0.5. You can actually create an a infinite number of interpolation function which work similar to the the listing above. The general definition of animation interpolations. which we shall name as an Easing, is as follows:

`type  TEasing = function(Percent: Float): Float;function Linear(Percent: Float): Float;begin  Result = Percent;end;`

But wait you say, aren't there supposed to be two values to blends between? Yes there are, and this is where an Interpolate function can be defined:

`{ Calculates the effect of an easing on values }function Interpolate(Easing: TEasing; Percent: Float; Start, Finish: Float): Float; begin  if Percent < 0 then    Result := Start  else if Percent > 1 then    Result := Finish  else  begin    Percent := Easing(Percent);    Result := Start * (1 - Percent) + Finish * Percent;  end;end;`

Now we can simple call Interpolate(Linear, Time, 0.5, 1.0) to receive our new button opacity when it is pressed. But wait, there's even more. Now that we have a general Interpolate function, we can substitute alternate easing functions. Here are a few:

`function Easy(Percent: Float): Float;begin  Result := Percent * Percent * (3 - 2 * Percent);end; function Drop(Percent: Float): Float;begin  Result := Percent * Percent;end;  function Spring(Percent: Float): Float;begin  Percent := Percent * Percent;  Result := Sin(PI * Percent * Percent * 10 - PI / 2) / 4;  Result := Result * (1 - Percent) + 1;  if Percent < 0.3 then    Result := Result * Easy(Percent / 0.3);end;`

In Cross Codebot we actually supply you with an extendable dictionary of easings, pre-populated with some easings already. To add you own easing simple write the following:

`{ Define our own easing function }function MyQuinticEase(Percent: Float): Float;begin  Percent := Percent - 1;  Result := 1 + Percent * Percent * Percent * Percent * Percent;end;procedure TForm1.FormCreate(Sender: TObject);begin  Easings['MyQuinticEase'] := MyQuinticEase;end;`

Result:

Now that you understand easings and interpolation, here is the program listing of the example at the top of this page:

`{ We are going to allow for variable speed playback }var  TimeFactor: Double;  TimeLast: Double;  TimeNow: Double;{ Normally you never use step functions for animations, but  in this case we don't want time to rewind or skip ahead  if time factor changes. So in this case we step time forward  only when a timer ticks. }procedure TimeStep;var  T: Double;begin  if TimeLast = 0 then    TimeLast := TimeQuery;  T := TimeQuery;  TimeNow := TimeNow + (T - TimeLast) * TimeFactor;  TimeLast := T;end;var  { The currently selected easing }  CurrentEasing: TEasing;  { The most basic of easings, linear }  Linear: TEasing;  { Our images }  Food: TArrayList<IBitmap>;const  { Our image sources }  FoodNames = 'apples,bananas,cherries,doughnuts,eggs,fish,grapes';procedure TForm1.FormCreate(Sender: TObject);var  B: IBitmap;  S: string;begin  { Default time to flow at a 1 to 1 rate }  TimeFactor := 1;  { Retrieve the linear easing function by name }  Linear := Easings['Linear'];  { Make the default current easing linear }  CurrentEasing := Linear;  { Our draw list will hold as many easings as are registered }  DrawList.Count := Easings.Count;  { Display 4 easings at a time }  DrawList.Height := DrawList.ItemHeight * 4 + 4;;  DrawList.ItemIndex := 0;  SizingPanel.Height := DrawList.Height;  { Load our food images }  for S in FoodNames.Split(',') do  begin    B := NewBitmap;    B.LoadFromFile(S + '.jpg');    Food.Push(B);  end;end;procedure TForm1.DrawListSelectItem(Sender: TObject);begin  { When you select an item in the draw list, change the current easing }  CurrentEasing := Easings.Items[DrawList.ItemIndex].Value;end;{ We are going to default the animation sequence to 2 seconds,  with a 0.5 second pause at both ends. This can be sped up or  slowed down using time factor }function AnimatedTime: Double;begin  Result := Remainder(TimeNow, 2);  if Result <= 0.5 then    Result := 0  else if Result >= 1.5 then    Result := 1  else    Result := Result - 0.5;end;{ This is where we draw the photos }procedure TForm1.SizingPanelRender(Sender: TObject; Surface: ISurface);const  { Space the photos 150 pixels apart }  Space = 150;  { Draw an individual photo }  procedure DrawPhoto(CurIndex, Index: Integer; Time: Float; Point: TPointF; Offset: Float);  var    Factor: Float;    S, D: TRectF;  begin    { Corrent the index to fall within the Food array bounds }    Index := (Index + Food.Length) mod Food.Length;    { Rhis is the default photo size }    Factor := 0.5;    { If the index is current, make it bigger }    if Index = CurIndex then      Factor := Interpolate(CurrentEasing, Time, Factor, 0.7, ReverseBox.Checked)    { Else if the index was current, make it smaller }    else if Index = (CurIndex + Food.Length - 1) mod Food.Length then      Factor := Interpolate(CurrentEasing, Time, 0.7, Factor, ReverseBox.Checked);    { Get the source rectangle }    S := Food[Index].ClientRect;    { And the dest rectangle }    D := S;    { Scale the dest rectangle by the factor }    D.Width := D.Width * Factor;    D.Height := D.Height * Factor;    { Center the photo at point }    D.Center(Point);    { Scroll the photo based on time and the easing }    D.Y := Offset + Interpolate(CurrentEasing, Time, D.Y - Space, D.Y, ReverseBox.Checked);    { Draw a white border }    D.Inflate(10, 10);    Surface.Rectangle(D);    Surface.Fill(NewBrush(clWhite));    { Draw the photo }    D.Inflate(-10, -10);    Food[Index].Surface.CopyTo(S, Surface, D);  end;var  Time: Float;  Index: Integer;  R: TRectI;  G: IGradientBrush;  I: Integer;begin  { Fill the rect with a black to gray gradient }  R := SizingPanel.ClientRect;  G := NewBrush(0, R.Top, 0, R.Bottom);  G.AddStop(clBlack, 0);  G.AddStop(clGray, 1);  Surface.FillRect(G, R);  { Get the animation timing }  Time := AnimatedTime;  { Get the current photo index based on TimeNow }  Index := Trunc(TimeNow / 2) mod Food.Length;  { And draw all the photos that might currently be visible }  for I := -2 to 2 do    DrawPhoto(Index, Index - I, Time, R.MidPoint, Space * I);end;{ Draw a background with a dashed set of time lines }procedure TForm1.DrawListDrawBackground(Sender: TObject; Surface: ISurface;  Rect: TRectI);const  Align = 0.5;var  Time: Float;  R: TRectF;  P: IPen;begin  { Fill with the current control color }  FillRectColor(Surface, Rect, DrawList.CurrentColor);  R := Rect;  { Create a silver pen }  P := NewPen(clSilver);  R.X := R.MidPoint.X + 10;  R.Width := Rect.Width - R.X - 30 - Align;  { Add the left time boundary to the path }  Surface.MoveTo(R.Left, R.Top);  Surface.LineTo(R.Left, R.Bottom);  { Add the left right time boundary to the path }  Surface.MoveTo(R.Right, R.Top);  Surface.LineTo(R.Right, R.Bottom);  { Stroke the time boundary }  Surface.Stroke(P);  { Switch to a red dashed pen }  P.Color := clRed;  P.LinePattern := pnDash;  Time := AnimatedTime;  { We'll draw the red dotten line as the actual time }  R.Left := Interpolate(Linear, Time, R.Left, R.Right);  Surface.MoveTo(R.Left, R.Top);  Surface.LineTo(R.Left, R.Bottom);  { And stroke the path with the red pen }  Surface.Stroke(P);end;{ This method draws the items in the TDrawList }procedure TForm1.DrawListDrawItem(Sender: TObject; Surface: ISurface;  Index: Integer; Rect: TRectI; State: TDrawState);var  KeyValue: TEasingKeyValue;  Time: Double;  R: TRectF;  A, B, C: TPointF;begin  { Retrieve the KeyValue from to Easings dicntionary }  KeyValue := Easings.Items[Index];  { If the item is selected, but it a nice gradiant outline and fill }  if dsSelected in State then    FillRectSelected(Surface, Rect, 5);  { Retrieve the current animation time }  Time := AnimatedTime;  { Define the rectangle where the easing should be drawn }  R := Rect;  R.Inflate(-10, -30);  R.Offset(0, -8);  R.Right := R.Left + 100;  R.Bottom := R.Bottom - 2;  { And give it to our Codebot.Graphics.DrawEasing procedure }  DrawEasing(Surface, Theme.Font, R, KeyValue.Value, ReverseBox.Checked, Time);  R.Top := R.Bottom + 8;  R.Bottom := Rect.Bottom;  { Draw the name beneath the easing }  Surface.TextOut(Theme.Font, KeyValue.Key, R, drCenter);  { Create a block to show the easing motion }  R := TRectF.Create(20, 20);  { Create two points A and B }  A := Rect.MidPoint;  A.Offset(10, 0);  B := A;  B.X := Rect.Right - 30;  C.Y := A.Y;  { And interpolate A and B to create C, using the X axis }  C.X := Interpolate(KeyValue.Value, Time, A.X, B.X, ReverseBox.Checked);  { Center our block on C }  R.Center(C);  { And fill it with black }  Surface.FillRect(NewBrush(clBlack), R);end;procedure TForm1.SlideBarChange(Sender: TObject);begin  { When the slider changes, update the caption }  SpeedLabel.Caption := 'Speed: %f'.Format([SlideBar.Position]);  { And the time factor }  TimeFactor := SlideBar.Position;end;procedure TForm1.TimerTimer(Sender: TObject);begin  { Advance time, but don't reverse it }  TimeStep;  { Redraw our controls }  DrawList.Invalidate;  SizingPanel.Invalidate;end;`

Xirax
Posts: 55
Joined: Sat Mar 07, 2015 11:16 am

### Re: Animating with easings

Its beautiful.
Great news that it can expand user's experience,bad new for me that I was working on a animating library as a gift to CodeBot and you made it faster Have a good day and don't forget answer me in other project.

Xirax
Posts: 55
Joined: Sat Mar 07, 2015 11:16 am

### Re: Animating with easings

Hi,

If we want when user click on food we show it's name what can we do?
I tried to replace painting each food with a TSizingPanel and animate them and use their OnClick event but it cause flickering.

sysrpl
Posts: 108
Joined: Thu Feb 05, 2015 6:31 pm

### Re: Animating with easings

Never animate by moving controls. Animate by moving graphics. To add a text when some event happens first you find the event, in this case mousedown and mouseup. TRectF.Contains will tell you if a point is inside a rect, so you can use that to find the photo. Then to reveal the photo name, use Surface.TextOut to draw the name.

With that you knowledge can define a type to store a photo's rectangle and draw photo state, supposing that when state is true a name is displayed, and when false it is not:

`type  TRectState = record    { Location of the photo }    Rect: TRectF;    { When true we show the photo name }    Named: Boolean;  end;var  { A list of states for each photo }  RectStates: array[0..6] of TRectState;  { Index of the photo being pressed }  RectDown: Integer = -1;`

Ask yourself this right now, do you know what comes next?

Answer: You fill out DrawList.MouseUp/DrawList.MouseDown and SizingPanelRender.DrawPhoto with the appropriate logic.

`procedure TForm1.SizingPanelMouseDown(Sender: TObject; Button: TMouseButton;  Shift: TShiftState; X, Y: Integer);var  I: Integer;begin  if Button = mbLeft then    for I := Low(RectStates) to High(RectStates) do      if RectStates[I].Rect.Contains(X, Y) then      begin        RectDown := I;        Break;      end;end;procedure TForm1.SizingPanelMouseUp(Sender: TObject; Button: TMouseButton;  Shift: TShiftState; X, Y: Integer);var  I: Integer;begin  if Button = mbLeft then  begin    if (RectDown > -1) and RectStates[RectDown].Rect.Contains(X, Y) then      RectStates[RectDown].Named := not RectStates[RectDown].Named;    RectDown := -1;  end;end;`

Xirax
Posts: 55
Joined: Sat Mar 07, 2015 11:16 am

### Re: Animating with easings

Thank you.
But why you say never animating controls? For this time your way do the job but for many other situations we will need animating control and I saw applications that animate controls without flicker. 