samples / callout.c¶
This not so short sample will pop-up a callout bubble with some text in it. It will demonstrate how to use image as a user interface skin, how to draw text and how to use an the timeline and easing functions for animations.
#include "cage.h"
Sample preparation code¶
For this sample we’ll need a background image, a font to draw text with, a stencil image that holds the skin of our callout box and a timeline to animate the pop-up effect of the callout.
static struct sample_data {
struct timeline* timeline;
struct image* stencil;
struct font* font;
struct image* background;
} sample_data = { NULL, NULL, NULL, NULL };
This is going to be the callout text:
static const char* CO_TEXT = "Welcome to Cage,\nthe elementary"
" game\ndevelopment library for\n"
"the C programming\nlanguage.";
And, this is where the callout will originate from, the callout arrow head position:
static const struct coords CO_ORIGIN = { 20, 40 };
static const struct coords CO_TIP_OFFSET = { 10, 0 };
These callback functions will be the events on the animation timeline. wait() will create a short pause when starting the sample, popup() will animate the appearance of the callout and show will simply draw the static callout.
void* popup(void* data, float elapsed_ms, float progress);
void* show(void* data, float elapsed_ms, float progress);
These are going to be the sample timeline events. The program will draw the background, wait for a second, then animate the callout during 1 second and finally, just draw the callout for 10 seconds. Easy peasy:
#define N_CO_EVENTS 2
static struct timeline_event CO_EVENTS[N_CO_EVENTS] = {
{ 1 * SECOND, popup, 1 * SECOND },
{ 0, show, 10 * SECONDS }
};
Creating the state data¶
We use the game state create() function to initialize the state data structure: the background image, the callout stencil, the font and the timeline.
#define SAFELY(c) \
if ((c) == NULL) goto error
#define IF_NEEDED(f, p) \
if (p != NULL) f(p)
static void* create_sample(void)
{
SAFELY(sample_data.background = create_image("res/bg.png"));
SAFELY(sample_data.stencil = create_image("res/callout.png"));
SAFELY(sample_data.font = create_font("res/cofont.png", 16, 16));
sample_data.font->line_spacing = 1;
sample_data.font->char_spacing = 1;
SAFELY(sample_data.timeline = create_timeline());
if (append_events(sample_data.timeline, N_CO_EVENTS, CO_EVENTS) == -1) {
ERROR("unable to append timeline events");
goto error;
}
return &sample_data;
error:
IF_NEEDED(destroy_image, sample_data.background);
IF_NEEDED(destroy_image, sample_data.stencil);
IF_NEEDED(destroy_font, sample_data.font);
IF_NEEDED(destroy_timeline, sample_data.timeline);
return NULL;
}
Drawing a callout box¶
The callout Stencil has the following skin components:
enum {
TOP_LEFT = 0,
TOP = 1,
TOP_EX = 2,
TOP_RIGHT = 3,
LEFT = 4,
FILL = 5,
FILL_COL_EX = 6,
RIGHT = 7,
LEFT_EX = 8,
FILL_ROW_EX = 9,
FILL_COL_ROW_EX = 10,
RIGHT_EX = 11,
BOTTOM_LEFT = 12,
BOTTOM = 13,
BOTTOM_EX = 14,
BOTTOM_RIGHT = 15
};
Each tile of the callout stencil has a 16x16 pixel size
#define GSIZE 16
#define GRID(X, Y) GSIZE *X, GSIZE *Y, GSIZE, GSIZE
The stencil has each component in these grid positions:
static struct rectangle rects[16] = {
{ GRID(0, 0) }, { GRID(2, 0) }, { GRID(2, 0) }, { GRID(4, 0) },
{ GRID(0, 1) }, { GRID(1, 1) }, { GRID(1, 1) }, { GRID(4, 1) },
{ GRID(0, 1) }, { GRID(1, 1) }, { GRID(1, 1) }, { GRID(4, 1) },
{ GRID(0, 2) }, { GRID(2, 2) }, { GRID(2, 2) }, { GRID(4, 2) }
};
This is how we draw the border tiles (corners and lines) of the callout.
static void draw_border(struct image* stencil, int x, int y, int w, int h)
{
int r, c;
int inner_cols;
int inner_rows;
inner_cols = w > GSIZE * 2 ? (w - (GSIZE * 2)) / GSIZE : 0;
inner_rows = h > GSIZE * 2 ? (h - (GSIZE * 2)) / GSIZE : 0;
/* draw corners */
draw_image(stencil, x, y, &rects[TOP_LEFT], 0);
draw_image(stencil, x + w - GSIZE, y, &rects[TOP_RIGHT], 0);
draw_image(stencil, x, y + h - GSIZE, &rects[BOTTOM_LEFT], 0);
draw_image(stencil, x + w - GSIZE, y + h - GSIZE, &rects[BOTTOM_RIGHT], 0);
/* draw borders */
for (r = 0; r <= inner_rows; r++) {
struct rectangle* left = &rects[LEFT];
struct rectangle* right = &rects[RIGHT];
if (r == inner_rows) {
left = &rects[LEFT_EX];
right = &rects[RIGHT_EX];
}
draw_image(stencil, x, y + GSIZE + r * GSIZE, left, 0);
draw_image(stencil, x + w - GSIZE, y + GSIZE + r * GSIZE, right, 0);
}
for (c = 0; c <= inner_cols; c++) {
struct rectangle* top = &rects[TOP];
struct rectangle* bottom = &rects[BOTTOM];
if (c == inner_cols) {
top = &rects[TOP_EX];
bottom = &rects[BOTTOM_EX];
}
draw_image(stencil, x + GSIZE + c * GSIZE, y, top, 0);
draw_image(stencil, x + GSIZE + c * GSIZE, y + h - GSIZE, bottom, 0);
}
}
This is how we fill the inner area of the callout.
static void fill_box(struct image* stencil, int x, int y, int w, int h)
{
int r, c;
int inner_cols;
int inner_rows;
inner_cols = w > GSIZE * 2 ? (w - (GSIZE * 2)) / GSIZE : 0;
inner_rows = h > GSIZE * 2 ? (h - (GSIZE * 2)) / GSIZE : 0;
for (r = 0; r <= inner_rows; r++) {
for (c = 0; c <= inner_cols; c++) {
struct rectangle* rect = &rects[FILL];
if (r == inner_rows && c == inner_cols)
rect = &rects[FILL_COL_ROW_EX];
else if (r == inner_rows)
rect = &rects[FILL_ROW_EX];
else if (c == inner_cols)
rect = &rects[FILL_COL_EX];
draw_image(stencil, x + GSIZE + c * GSIZE, y + GSIZE + r * GSIZE,
rect, 0);
}
}
}
This will draw the callout box, without the callout tip
static void draw_box(void* data, int x, int y, int w, int h)
{
rects[FILL_ROW_EX].h = rects[FILL_COL_ROW_EX].h = ((h - GSIZE * 2) % GSIZE);
rects[FILL_COL_EX].w = rects[FILL_COL_ROW_EX].w = ((w - GSIZE * 2) % GSIZE);
rects[LEFT_EX].h = rects[RIGHT_EX].h = ((h - GSIZE * 2) % GSIZE);
rects[TOP_EX].w = rects[BOTTOM_EX].w = ((w - GSIZE * 2) % GSIZE);
draw_border(data, x, y, w, h);
fill_box(data, x, y, w, h);
}
This will draw the callout box and the callout tip
static void draw_callout(void* data, int x, int y, int w, int h)
{
struct rectangle callout = { GRID(1, 2) };
draw_box(data, x, y, w, h);
draw_image(data, x + 2 * GSIZE, y + h - GSIZE, &callout, 0);
}
Updating the frames¶
For each frame, the update state function will draw the background image and then use the timeline to activate the sample events for pop-up the callback and continue drawing it for a few seconds. We explicitly eliminate any unused parameter warning using the UNUSED macro.
static void update_sample(void* data, float elapsed_ms)
{
struct rectangle clip = { 0, 0, 192, 108 };
struct sample_data* sdata = data;
screen_color(color_from_RGB(120, 120, 120));
draw_image(sdata->background, 0, 0, &clip, 0);
update_timeline(sdata->timeline, sdata, elapsed_ms);
UNUSED(elapsed_ms);
}
void* popup(void* data, float elapsed_ms, float progress)
{
int x, y;
struct sample_data* sdata = data;
float bounce = (bounce_ease_out(progress));
int w, h;
measure_text(sdata->font, CO_TEXT, &w, &h);
x = w / 2 + CO_ORIGIN.x - ((w / 2 - CO_TIP_OFFSET.x) * bounce);
y = h / 2 + CO_ORIGIN.y - ((h / 2 + h - CO_TIP_OFFSET.y) * bounce);
draw_box(sdata->stencil, x, y, (w * bounce) + 32, (h * bounce) + 32);
UNUSED(elapsed_ms);
return NULL;
}
void* show(void* data, float elapsed_ms, float progress)
{
int x, y;
struct sample_data* sdata = data;
int w, h;
measure_text(sdata->font, CO_TEXT, &w, &h);
x = w / 2 + CO_ORIGIN.x - ((w / 2 - CO_TIP_OFFSET.x) * 1.0f);
y = h / 2 + CO_ORIGIN.y - ((h / 2 + h - CO_TIP_OFFSET.y) * 1.0f);
draw_callout(sdata->stencil, x, y, w + 32, h + 32);
draw_text(sdata->font, CO_TEXT, x + 16, y + 16);
UNUSED(progress);
UNUSED(elapsed_ms);
return NULL;
}
Destroying and finalizing¶
When exiting the sample, the destroy_sample() function will destroy any allocated resources.
static void destroy_sample(void* data)
{
struct sample_data* sdata = data;
destroy_image(sdata->stencil);
destroy_font(sdata->font);
destroy_timeline(sdata->timeline);
destroy_image(sdata->background);
}
Finally, the main¶
Finally, the game’s main function delegates the execution to Cage’s game_loop() function together with the 3 state functions we wrote.
int main(void)
{
return game_loop(create_sample, update_sample, destroy_sample);
}