Creating animations

From SEGGER Wiki
Revision as of 16:31, 2 April 2020 by Florian (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

The following tutorial will demonstrate and explain in detail how animations can be created and executed in emWin.

Button triggering an animation.

Objective of this tutorial

The purpose of this tutorial is an in-depth explanation on how animations are created in emWin.

The final application that is written in this tutorial will be a small window, that can be moved in and out of the screen with an animation. This animation will be triggered by a button click.

Tutorial

Prerequirements

Before it will be explained how the animation is created, we are going to create the basis of the application. This means, we are going to create the windows and widgets necessary for the application.

First, we need to include the file DIALOG.h, since we are using the Window Manager and widgets.

#include "DIALOG.h"

The MainTask() has to initialize emWin and keep it alive in a superloop. Then, multi-buffering should be enabled by calling WM_MULTIBUF_Enable(1). Next, we call WM_SetCallback(WM_HBKWIN, _cbBk) to set a custom-defined callback _cbBk() to the background window. The definition of the window callback _cbWindow() and the background window callback _cbBk() will follow.

void MainTask(void) {
  WM_HWIN hWin;

  //
  // Init emWin.
  //
  GUI_Init();
  //
  // Enable multi-buffering to avoid flickering during the animation.
  //
  WM_MULTIBUF_Enable(1);
  //
  // Set custom callback routine to background window.
  //
  WM_SetCallback(WM_HBKWIN, _cbBk);

  while (1) {
    GUI_Delay(100);
  }
}

Now, we need to define the callback for the background window. As the first step, we add a WM_SET_CALLBACK case. We will land here right after the callback _cbBk() has been set to the background window. Here, we create the window that will be moved by the animation and a button that triggers the animation. In any window that has been created normally, we could react on the WM_CREATE case, but since the background window is created upon emWin's initialization, we are using WM_SET_CALLBACK. The handle of the created window is saved as the static variable hWin. Both the button and window are child windows of the background window.

Then, the callback needs to react on the WM_PAINT message and clear the entire window with white. We do this, so emWin can redraw the background window when our animated window is moved. Otherwise if the background window would not be redrawn, there would be leftover marks on the screen.

We also add a case for WM_NOTIFY_PARENT, since the button has been specified as a child window of the background window. This means button clicks will be sent via the WM_NOTIFY_PARENT message to the button's parent window, therefore this callback.

static void _cbBk(WM_MESSAGE * pMsg) {
  int            Id, NCode;
  WM_HWIN        hItem;
  static WM_HWIN hWin;

  switch(pMsg->MsgId) {
  case WM_SET_CALLBACK:
    //
    // Create a window that will be moved using an animation.
    //
    hWin = WM_CreateWindowAsChild(-50, 50, 50, 150, WM_HBKWIN, WM_CF_SHOW, _cbWindow, 0);
    //
    // Create button to trigger the animation.
    //
    hItem = BUTTON_CreateEx(10, 10, 80, 20, WM_HBKWIN, WM_CF_SHOW, 0, GUI_ID_BUTTON0);
    BUTTON_SetText(hItem, "Move in/out");
    break;
  case WM_PAINT:
    GUI_SetBkColor(GUI_WHITE);
    GUI_Clear();
    break;
  case WM_NOTIFY_PARENT:
    Id    = WM_GetId(pMsg->hWinSrc);
    NCode = pMsg->Data.v;
    switch(Id) {
    case GUI_ID_BUTTON0:
      switch (NCode) {
      case WM_NOTIFICATION_RELEASED:
        break;
      }
      break;
    }
    break;
  default:
    WM_DefaultProc(pMsg);
  }
}

Finally, we define the callback of the window that we want to move. The window should appear red.

static void _cbWindow(WM_MESSAGE * pMsg) {
  switch (pMsg->MsgId) {
  case WM_PAINT:
    GUI_SetBkColor(GUI_RED);
    GUI_Clear();
    break;
  default:
    WM_DefaultProc(pMsg);
    break;
  }
}

Set up animation data

Before we can create the animation in emWin, we need to think about what kind of data will be needed during the animation of the window. In this case, we are defining a custom structure that holds the handle of the window we want to move. The structure should also contain the starting position on the x-axis and the end position, since the window will be moved only on the x-axis. It makes sense to also store the y-position of the window.

typedef struct {
  WM_HWIN hWin;
  int     xPosStart;
  int     xPosDest;
  int     yPos;
} ANIM_DATA;

It also makes sense to add a define for the duration of the animation.

#define ANIM_PERIOD    500

Create the animation

In the next step, we are going to create the actual animation. First, we are going to create some variables for the animation handle and the animation data. These variables have to be static so that the data stays persistent during the animation.

static GUI_ANIM_HANDLE hAnim;   // Animation handle.
static ANIM_DATA       Data;    // Data about the window's position.
static U8              MovedIn; // Flag that saves whether the window is currently moved in or out.

Then, we go into the background window's callback _cbBk() to the WM_NOTIFY_PARENT case. As already mentioned before, we check if the button which sends the WM_NOTIFY_PARENT message matches the trigger button and the notification code matches WM_NOTIFICATION_RELEASED. Now, the animation data structure ANIM_DATA should be initialized with the necessary data.

Data.hWin      = hWin;                   // Handle of window to be moved.
Data.xPosStart = WM_GetWindowOrgX(hWin); // Current x-position.
Data.yPos      = WM_GetWindowOrgY(hWin); // Current y-position.

The destination position on the x-axis depends on whether or not the window is already moved out. The flag that saves this information should be checked, if the window is moved out, the x-position should be 10, so that the window will be visible. If it isn't moved out, the position should be -50, so that it won't be visible.

if (!MovedIn) {
  Data.xPosDest = 10;
} else {
  Data.xPosDest = -50;
}

Now that the animation data is set up, create an animation handle. The first parameter states how long the entire animation will be, which we can use the macro ANIM_PERIOD for. The next parameter is the minimum time per frame (25 ms are sufficient). As the third parameter we can pass a custom void pointer to the delete and slice callback routines (more on these routines later). Since the state of the window can be different when the animation is finished, we pass a pointer to the MovedIn flag, so we can later access it in the delete callback routine. The last parameter can be used to pass a function pointer to a slice callback which is explained in the user manual.

hAnim = GUI_ANIM_Create(ANIM_PERIOD, 25, (void *)&MovedIn, NULL);
Animation items on the timeline of an animation.

The next step is to add animation items to the animation we just created. It is possible to add multiple items to an animation, but in this example we just add one. An animation item defines what should be done in the animation during the given timeline. The image on the right demonstrates different animations items on the timeline of an animation.

The animation handle is passed as first parameter. The next two parameters define the start and end point of the item on the timeline. Between this given time, the item callback will be executed. Since we just have one item, we enter 0 and ANIM_PERIOD, because this item should be executed from start to end of the animation. If you had multiple items, you would enter start and end points on the timeline, multiple items could even run simultaneously.

GUI_ANIM_AddItem(hAnim, 0, ANIM_PERIOD, ANIM_ACCELDECEL, (void *)&Data, _AnimMoveWindow);

Now, a pointer to the routine that calculates the position value of the animation should be passed. With this routine, the animation is told how it should progress over time, for example if the animation should progress linear, accelerate etc. For this animation, we pick the predefined macro ANIM_ACCELDECEL. With this positioning, the window will first be accelerated and then slowed down during its movement. It is also possible to pass a pointer to a custom defined position calculation routine, but more on that later. Finally, we pass a pointer to the animation data that we need in the animation function and a pointer to the animation function _AnimMoveWindow().

The last routine that has to be called is GUI_ANIM_StartEx(), this makes sure that the animation will be started automatically. The second parameter sets the number of loops of the animation. Lastly, we pass a function pointer to the delete callback routine.

GUI_ANIM_StartEx(hAnim, 1, _OnDelete);

With the current implementation, every button release creates and starts a new animation. If we had the same animation running multiple times simultaneously, they would interfere with each other. To prevent this from happening, we should check if the animation is already running by using the routine GUI_ANIM_IsRunning().

//
// Check if animation is already running.
//
if (GUI_ANIM_IsRunning(hAnim) == 0) {
  //
  // Set up animation data.
  //
  Data.hWin      = hWin;                   // Handle of window to be moved.
  Data.xPosStart = WM_GetWindowOrgX(hWin); // Current x-position.
  Data.yPos      = WM_GetWindowOrgY(hWin); // Current y-position.
  if (!MovedIn) {
    Data.xPosDest = 10;
  } else {
    Data.xPosDest = -50;
  }
  //
  // Create animation handle.
  //
  hAnim = GUI_ANIM_Create(ANIM_PERIOD, 25, &MovedIn, NULL);
  //
  // Add animation item.
  //
  GUI_ANIM_AddItem(hAnim, 0, ANIM_PERIOD, ANIM_ACCELDECEL, &Data, _AnimMoveWindow);
  //
  // Start animation for one time.
  //
  GUI_ANIM_StartEx(hAnim, 1, _OnDelete);
}

Create the routine(s) for animation item(s)

Our penultimate step is adding the required animation routine to the application. We create the routine _AnimMoveWindow(), that has the following prototype.

static void _AnimMoveWindow(GUI_ANIM_INFO * pInfo, void * pVoid);

The custom void pointer pVoid should be casted to our custom defined ANIM_DATA type, since we passed it earlier when adding the animation item. The value pInfo->Pos contains the current position calculated by the animation. This value will be increased from 0 to GUI_ANIM_RANGE during the entire animation, therefore it will be used for calculating the current position of the window.

We calculate the new x-position based on the starting position and add the distance that we want to move the window. This distance has to be multiplied with the pInfo->Pos value and then divided by the maximum value GUI_ANIM_RANGE, which is 32767. This leaves us with a new x-position we can use which is based on the current state of the animation.

pData = (ANIM_DATA *)pVoid;
//
// Calculate new window position with the ANIM_DATA struct
//
xPos = pData->xPosStart + ((pData->xPosDest - pData->xPosStart) * pInfo->Pos) / GUI_ANIM_RANGE;

Since the y-position will not be changed by the animation, we can just retrieve it with the window handle. Now that we have the x- and y-position, we can move the window.

//
// Get current y-position since it won't be changed with the animation.
//
yPos = WM_GetWindowOrgY(pData->hWin);
//
// Move window to new position
//
WM_MoveTo(pData->hWin, xPos, yPos);

When finished, the animation routine should look like shown below.

static void _AnimMoveWindow(GUI_ANIM_INFO * pInfo, void * pVoid) {
  ANIM_DATA * pData;
  int         xPos;

  pData = (ANIM_DATA *)pVoid;
  //
  // Calculate new window position with the ANIM_DATA struct.
  //
  xPos = pData->xPosStart + ((pData->xPosDest - pData->xPosStart) * pInfo->Pos) / GUI_ANIM_RANGE;
  //
  // Move window to new position.
  //
  WM_MoveTo(pData->hWin, xPos, pData->yPos);
}

Create the delete callback routine

The last step we need to run our application is adding a delete callback. This callback routine should look like shown below.

static void _OnDelete(void * pVoid) {
  U8 * pMovedIn;
  
  //
  // Get custom void pointer. Toggle the flag that saves if the window is moved in.
  //
  pMovedIn = (U8 *)pVoid;
  if(pMovedIn) {
    *pMovedIn ^= 1;
  }
}

Via the custom void pointer, we can access our flag that saves if the window is moved in or out. The delete callback will be called, when the animation has finished, this means now we can toggle the flag.

Now, we can compile and run our application. When clicking the button, the window will be moved in or out.

Custom defined position calculation routine

Optionally, if needed, instead of the predefined position calculation routines, a custom routine may also be used. The routine is called by the animation object each time a single position value is required. This routine should calculate a value in dependence of the start time, end time and current time of the animation item.

The routine should fit the prototype shown below.

I32 _CalcPosition(GUI_TIMER_TIME ts, GUI_TIMER_TIME te, GUI_TIMER_TIME tNow);

Below is a custom implementation that uses a sine curve for calculating the values.

I32 _Anim_Sin(GUI_TIMER_TIME ts, GUI_TIMER_TIME te, GUI_TIMER_TIME tNow) {
  I32 Result;
  I32 SinHQ;

  SinHQ  = GUI__SinHQ((tNow * 180000) / (te - ts));
  Result = (SinHQ * GUI_ANIM_RANGE) >> 16;
  return Result;
}

The custom defined routine can then be passed to the animation item.

GUI_ANIM_AddItem(hAnim, 0, ANIM_PERIOD, _Anim_Sin, &Data, _AnimMoveWindow);

Downloads

You can download the source code of this example here.