Animating with easings

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

Animating with easings

Postby sysrpl » Tue Mar 24, 2015 1:11 am


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

Postby Xirax » Tue Mar 24, 2015 6:26 pm

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 ;)
Please add these demo so I can play more with them.
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

Postby Xirax » Sun Apr 12, 2015 12:27 pm

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: 105
Joined: Thu Feb 05, 2015 6:31 pm

Re: Animating with easings

Postby sysrpl » Sun Apr 12, 2015 12:59 pm

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

Postby Xirax » Sun Apr 12, 2015 1:06 pm

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.


Return to “Cross Codebot”

Who is online

Users browsing this forum: No registered users and 2 guests