Last active
June 4, 2019 16:39
-
-
Save idiotWu/fb6b084fab6db0d6c4ab60b27e6ddfa6 to your computer and use it in GitHub Desktop.
Simple timeline-based animation engine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Timeline based animation engine | |
* | |
* t | |
* ------+------------------------------------------> timeline | |
* +-----|-+ | |
* | A | | <- animation{} | |
* +-----|-+ | |
* +--|------+ | |
* | | A | | |
* +--|------+ | |
* +----|+-----+-----+-----+-----+ | |
* | A || A | A | A | A | <- repeating animation | |
* +----|+-----+-----+-----+-----+ | |
* | | |
* render(t) | |
*/ | |
#include <stdlib.h> | |
#include <stdint.h> | |
#include <stdbool.h> | |
#include <assert.h> | |
#include <math.h> | |
#include "engine.h" | |
#include "list.h" | |
defNode(TimelineNode, Animation); | |
// typedef struct TimelineNode { | |
// Animation* data; | |
// struct TimelineNode* prev; | |
// struct TimelineNode* next; | |
// } TimelineNode; | |
defList(Timeline, TimelineNode); | |
// typedef struct Timeline { | |
// size_t count; | |
// TimelineNode* head; | |
// TimelineNode* tail; | |
// } Timeline; | |
static Timeline gameTL; | |
// reset state to the beginning of animaiton | |
static void resetFrame(Animation* animation) { | |
animation->currentFrame = 1; | |
animation->elapsed = 0; | |
} | |
/** | |
* Right-continuous animation (repeat > 1): | |
* - jumps to the first frame when the animation ends; | |
* - renders one more frame per sencond. | |
* | |
* 3 frames non-repeat: (3 FPS) | |
* [-------|-------] | |
* 1 2 3 | |
* | |
* 3 frames * 2 repeats: (3+1 FPS) | |
* [----|----|----][----|----|----] | |
* 1 2 3 1(4) 2 3 1(4) | |
*/ | |
Animation* createAnimation(uint16_t frameCount, | |
double duration, | |
uint16_t repeat) { | |
assert(frameCount > 0); | |
assert(repeat > 0); | |
Animation* animation = malloc(sizeof(Animation)); | |
resetFrame(animation); | |
animation->frameCount = frameCount; | |
animation->framesPerSecond = repeat == 1 ? frameCount : frameCount + 1; | |
animation->interval = round(duration / ANIMATION_60_FPS / animation->framesPerSecond); | |
animation->nth = 1; | |
animation->repeat = repeat; | |
animation->from = NULL; | |
animation->to = NULL; | |
animation->render = NULL; | |
animation->complete = NULL; | |
TimelineNode* node = (TimelineNode*)createNode(); | |
node->data = animation; | |
listAppend((List*)&gameTL, (Node*)node); | |
return animation; | |
} | |
Animation* createAnimation60FPS(double duration, uint16_t repeat) { | |
return createAnimation(round(duration / ANIMATION_60_FPS), duration, repeat); | |
} | |
bool cancelAnimation(Animation* animation) { | |
TimelineNode* node = gameTL.head; | |
while (node) { | |
if (node->data == animation) { | |
listDelete((List*)&gameTL, (Node*)node); | |
return true; | |
} | |
node = node->next; | |
} | |
return false; | |
} | |
// render next frame | |
void engineNext(void) { | |
if (!gameTL.count) { | |
return; | |
} | |
TimelineNode* node = gameTL.head; | |
TimelineNode* next; | |
do { | |
// record `next` in case that node is deleted | |
next = node->next; | |
Animation* animation = node->data; | |
animation->render(animation); | |
animation->elapsed++; | |
// elapsed < interval | |
// -> waiting | |
if (animation->elapsed < animation->interval) { | |
continue; | |
} | |
// elapsed == interval | |
// -> next frame | |
animation->currentFrame++; | |
animation->elapsed = 0; | |
// currentFrame < framesPerSecond | |
// -> running | |
if (animation->currentFrame < animation->framesPerSecond) { | |
continue; | |
} | |
// currentFrame == framesPerSecond | |
// -> repeat or finish | |
// repeating mode: jump to the first frame | |
if (animation->repeat > 1) { | |
resetFrame(animation); | |
} | |
// should repeat? | |
if (animation->repeat == ANIMATION_INFINITY || | |
animation->nth < animation->repeat) { | |
animation->nth++; | |
} else { | |
// finished | |
// render the last frame | |
animation->render(animation); | |
if (animation->complete) { | |
animation->complete(animation); | |
} | |
free(animation->from); | |
free(animation->to); | |
listDelete((List*)&gameTL, (Node*)node); | |
} | |
} while ((node = next) != NULL); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef ENGINE_H | |
#define ENGINE_H | |
#include <stdint.h> | |
#include <stdbool.h> | |
#define ANIMATION_60_FPS (1000.0 / 60.0) | |
#define ANIMATION_ONCE 1 | |
#define ANIMATION_INFINITY UINT16_MAX | |
typedef struct Animation { | |
void* from; | |
void* to; | |
uint16_t currentFrame; | |
uint16_t frameCount; | |
uint16_t framesPerSecond; | |
uint16_t elapsed; | |
uint16_t interval; | |
uint16_t nth; | |
uint16_t repeat; | |
void (*render)(struct Animation*); // render function | |
void (*complete)(struct Animation*); // on complete callback function | |
} Animation; | |
Animation* createAnimation(uint16_t frames, | |
double duration, | |
uint16_t repeat); | |
Animation* createAnimation60FPS(double duration, uint16_t repeat); | |
bool cancelAnimation(Animation* animation); | |
// render next frame | |
void engineNext(void); | |
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <stdlib.h> | |
#include <assert.h> | |
#include "list.h" | |
void listAppend(List* list, Node* node) { | |
if (list->count == 0) { | |
list->head = list->tail = node; | |
} else { | |
list->tail->next = node; | |
node->prev = list->tail; | |
list->tail = node; | |
} | |
list->count++; | |
} | |
void listPrepend(List* list, Node* node) { | |
if (list->count == 0) { | |
list->head = list->tail = node; | |
} else { | |
list->head->prev = node; | |
node->next = list->head; | |
list->head = node; | |
} | |
list->count++; | |
} | |
void listInsertAfter(List* list, Node* prev, Node* node) { | |
Node* next = prev->next; | |
prev->next = node; | |
node->prev = prev; | |
next->prev = node; | |
node->next = next; | |
list->count++; | |
} | |
void listDelete(List* list, Node* node) { | |
assert(list->count > 0); | |
Node* prev = node->prev; | |
Node* next = node->next; | |
if (list->head == node) { | |
list->head = next; | |
} | |
if (list->tail == node) { | |
list->tail = prev; | |
} | |
if (prev != NULL) { | |
prev->next = next; | |
} | |
if (next != NULL) { | |
next->prev = prev; | |
} | |
node->next = node->prev = NULL; | |
listFreeNode(node); | |
list->count--; | |
} | |
void listFreeNode(Node* node) { | |
free(node->data); | |
free(node); | |
} | |
void listDestory(List* list) { | |
Node* node = list->head; | |
while (node) { | |
listFreeNode(node); | |
node = node->next; | |
} | |
free(list); | |
} | |
List* createList(void) { | |
List* list = malloc(sizeof(List)); | |
list->count = 0; | |
list->head = list->tail = NULL; | |
return list; | |
} | |
Node* createNode(void) { | |
Node* node = malloc(sizeof(Node)); | |
node->data = NULL; | |
node->prev = node->next = NULL; | |
return node; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Generic Doubly-linked List | |
*/ | |
#ifndef LIST_H | |
#define LIST_H | |
#include <stddef.h> | |
#include <assert.h> | |
#define __HAS_ELEMENT(TYPE_A, TYPE_B, ELEMENT) \ | |
assert(offsetof(TYPE_A, ELEMENT) == offsetof(TYPE_B, ELEMENT)) | |
// duck typing check | |
#define SAFE_DUCK_LIST(DuckList) do {\ | |
__HAS_ELEMENT(DuckList, List, count);\ | |
__HAS_ELEMENT(DuckList, List, head);\ | |
__HAS_ELEMENT(DuckList, List, tail);\ | |
} while(0) | |
#define SAFE_DUCK_NODE(DuckNode) do {\ | |
__HAS_ELEMENT(DuckNode, Node, data);\ | |
__HAS_ELEMENT(DuckNode, Node, prev);\ | |
__HAS_ELEMENT(DuckNode, Node, next);\ | |
} while(0) | |
#define defNode(NAME, DATA_TYPE) \ | |
typedef struct NAME {\ | |
DATA_TYPE* data;\ | |
struct NAME* prev;\ | |
struct NAME* next;\ | |
} NAME | |
#define defList(NAME, NODE_TYPE) \ | |
typedef struct NAME {\ | |
size_t count;\ | |
NODE_TYPE* head;\ | |
NODE_TYPE* tail;\ | |
} NAME | |
defNode(Node, void); | |
//typedef struct Node { | |
// void* data; | |
// struct Node* prev; | |
// struct Node* next; | |
//} Node; | |
defList(List, Node); | |
//typedef struct List { | |
// size_t count; | |
// Node* head; | |
// Node* tail; | |
//} List; | |
void listAppend(List* list, Node* node); | |
void listPrepend(List* list, Node* node); | |
void listInsertAfter(List* list, Node* prev, Node* node); | |
void listDelete(List* list, Node* node); | |
void listFreeNode(Node* node); | |
void listDestory(List* list); | |
List* createList(void); | |
Node* createNode(void); | |
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#define _USE_MATH_DEFINES | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <math.h> | |
#include <time.h> | |
#include "glut.h" | |
#include "engine.h" | |
#define RENDER_WAVE | |
#define LISTEN_MOUSE_STAR | |
#define deg2rad(x) (x / 180.0 * M_PI) | |
#define UNUSED(x) (void)(x) | |
#define RECT_COUNT 60 | |
#define RECT_WIDTH (2.0 / RECT_COUNT) | |
typedef struct Rectangle { | |
double x1; | |
double y1; | |
double x2; | |
double y2; | |
double color[3]; | |
} Rectangle; | |
typedef struct Star { | |
double x; | |
double y; | |
double r; | |
double rot; | |
double color[4]; | |
} Star; | |
static int randInitFlag = 0; | |
double randomBetween(double lower, double upper) { | |
// init once | |
if (!randInitFlag) { | |
randInitFlag = 1; | |
srand((unsigned int)time(NULL)); | |
} | |
return (double)rand() / (double)RAND_MAX * (upper - lower) + lower; | |
} | |
double easeInOutBack(double p) { | |
if (p < 0.5) { | |
double f = 2 * p; | |
return 0.5 * (f * f * f - f * sin(f * M_PI)); | |
} else { | |
double f = (1 - (2*p - 1)); | |
return 0.5 * (1 - (f * f * f - f * sin(f * M_PI))) + 0.5; | |
} | |
} | |
// render sin wave | |
void renderWave(Animation* animation) { | |
Rectangle* rect = animation->from; | |
double percent = (double)animation->currentFrame / animation->frameCount; | |
double pFix = percent < 0.5 ? (percent * 2) : (1 - (percent - 0.5) * 2); | |
double pos = easeInOutBack(pFix); | |
if (animation->currentFrame == 1) { | |
rect->color[0] = randomBetween(0.5, 1); | |
rect->color[1] = randomBetween(0.5, 1); | |
rect->color[2] = randomBetween(0.5, 1); | |
} | |
glColor3dv(rect->color); | |
glRectd( | |
rect->x1, | |
rect->y1, | |
rect->x2, | |
rect->y1 + (rect->y2 - rect->y1) * pos); | |
} | |
// create sin wave | |
static int lastOffset = -1; | |
void createWave(Animation* animation) { | |
double interval = (double)(animation->frameCount) / RECT_COUNT; | |
int offset = animation->currentFrame / interval; | |
if (offset > lastOffset) { | |
lastOffset = offset; | |
Rectangle* rect = malloc(sizeof(Rectangle)); | |
double offset = RECT_WIDTH * (int)((animation->currentFrame - 1) / interval); | |
double height = sin((double)animation->currentFrame / (animation->frameCount) * M_PI) * 1.0 + 0.25; | |
rect->x1 = -1.0 + offset; | |
rect->y1 = -1.0; | |
rect->x2 = rect->x1 + RECT_WIDTH; | |
rect->y2 = -1.0 + height; | |
Animation* a = createAnimation60FPS(1500, ANIMATION_INFINITY); | |
a->from = rect; | |
a->render = renderWave; | |
} | |
} | |
// render falling star | |
const double starVertexes[5] = { 90.0, 234.0, 18.0, 162.0, 306.0 }; | |
void renderStar(Animation* animation) { | |
Star* star = animation->from; | |
star->r *= 0.95; | |
star->y -= 1.0/60.0; | |
star->rot += 10.0; | |
star->color[3] = 1.0 - (double)animation->currentFrame / animation->frameCount; | |
double scaleX = (double)glutGet(GLUT_WINDOW_WIDTH) / glutGet(GLUT_WINDOW_HEIGHT); | |
glColor4dv(star->color); | |
glBegin(GL_TRIANGLE_FAN); | |
glScaled(10.0, 1.0, 1.0); | |
glVertex2d(star->x, star->y); | |
for (size_t i = 0; i <= 5; i++) { | |
double rad = deg2rad(star->rot +starVertexes[i % 5]); | |
glVertex2d(star->x + star->r / scaleX * cos(rad), star->y + star->r * sin(rad)); | |
} | |
glEnd(); | |
} | |
// create star on mouse move | |
void mouseListenerStar(int x, int y) { | |
double u = (double)x / glutGet(GLUT_WINDOW_WIDTH) * 2.0 - 1.0; | |
double v = -(double)y / glutGet(GLUT_WINDOW_HEIGHT) * 2.0 + 1.0; | |
Star* star = malloc(sizeof(Star)); | |
star->x = u; | |
star->y = v; | |
star->r = randomBetween(0.05, 0.1); | |
star->rot = randomBetween(0.0, 360.0); | |
star->color[0] = randomBetween(0.5, 1); | |
star->color[1] = randomBetween(0.5, 1); | |
star->color[2] = randomBetween(0.5, 1); | |
Animation* animation = createAnimation60FPS(2000, 1); | |
animation->from = star; | |
animation->render = renderStar; | |
} | |
void init(void) { | |
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); | |
glEnable(GL_BLEND); | |
glClearColor(0.0, 0.0, 0.0, 0.0); | |
#ifdef RENDER_WAVE | |
Animation* animation = createAnimation60FPS(1000, 1); | |
animation->render = createWave; | |
#endif | |
#ifdef LISTEN_MOUSE_STAR | |
glutMotionFunc(mouseListenerStar); | |
glutPassiveMotionFunc(mouseListenerStar); | |
#endif | |
} | |
void update(int _) { | |
glutPostRedisplay(); | |
UNUSED(_); | |
glutTimerFunc(ANIMATION_60_FPS, update, 0); | |
} | |
void display(void) { | |
glClear(GL_COLOR_BUFFER_BIT); | |
engineNext(); | |
glutSwapBuffers(); | |
} | |
void reshape(int w, int h) { | |
glViewport(0, 0, w, h); | |
glMatrixMode(GL_PROJECTION); | |
glLoadIdentity(); | |
gluOrtho2D(-1.0, 1.0, -1.0, 1.0); | |
glMatrixMode(GL_MODELVIEW); | |
} | |
int main(int argc, char* argv[]) { | |
glutInit(&argc, argv); | |
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA); | |
glutInitWindowSize(800, 800); | |
glutCreateWindow("Test"); | |
glutDisplayFunc(display); | |
glutReshapeFunc(reshape); | |
init(); | |
update(0); | |
glutMainLoop(); | |
return EXIT_SUCCESS; | |
} |
Usage Example:
void renderRect(Animation* animation) {
double percent = (double)animation->currentFrame / animation->frameCount;
double c = percent < 0.5 ? (percent * 2) : (1 - (percent - 0.5) * 2);
printf("%2d/%2d\t%.2lf\n", animation->currentFrame, animation->frameCount, c);
glColor3d(0.75 + c * 0.25, c, 1 - c);
glRectd(-1.0 + c, -0.5, c, 0.5);
}
void display(void) {
glClear(GL_COLOR_BUFFER_BIT);
engineNext();
glutSwapBuffers();
}
int main(void) {
// ...
glutDisplayFunc(display);
Animation* animation = createAnimation60FPS(1000, ANIMATION_INFINITY);
animation->render = renderRect;
// ...
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Screencast: https://www.youtube.com/watch?v=LmbhUKNTtoc