14 KiB
layout | tutorial | tutorial_part | title | description | permalink | generate_toc | platform_choice | platforms | ||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
tutorials/tutorial | advanced | 1 | Vector Animations | How to use vector images in icons and animations. | /tutorials/advanced/vector-animations/ | true | true |
|
Some of the best Pebble apps make good use of the Animation
and the
Graphics Context
to create beautiful and eye-catching user
interfaces that look better than those created with just the standard Layer
types.
Taking a good design a step further may involve using the Draw Commands
API
to load vector icons and images, and to animate them on a point-by-point basis
at runtime. An additional capability of the Draw Commands
API is the draw
command sequence, allowing multiple frames to be incorporated into a single
resource and played out frame by frame.
This tutorial will guide you through the process of using these types of image files in your own projects.
What Are Vector Images?
As opposed to bitmaps which contain data for every pixel to be drawn, a vector file contains only instructions about points contained in the image and how to draw lines connecting them up. Instructions such as fill color, stroke color, and stroke width are also included.
Vector images on Pebble are implemented using the Draw Commands
APIs, which
load and display PDC (Pebble Draw Command) images and sequences that contain
sets of these instructions. An example is the weather icon used in weather
timeline pins. The benefit of using vector graphics for this icon is that is
allows the image to stretch in the familiar manner as it moves between the
timeline view and the pin detail view:
By including two or more vector images in a single file, an animation can be created to enable fast and detailed animated sequences to be played. Examples can be seen in the Pebble system UI, such as when an action is completed:
The main benefits of vectors over bitmaps for simple images and icons are:
-
Smaller resource size - instructions for joining points are less memory expensive than per-pixel bitmap data.
-
Flexible rendering - vector images can be rendered as intended, or manipulated at runtime to move the individual points around. This allows icons to appear more organic and life-like than static PNG images. Scaling and distortion is also made possible.
-
Longer animations - a side benefit of taking up less space is the ability to make animations longer.
However, there are also some drawbacks to choosing vector images in certain cases:
-
Vector files require more specialized tools to create than bitmaps, and so are harder to produce.
-
Complicated vector files may take more time to render than if they were simply drawn per-pixel as a bitmap, depending on the drawing implementation.
Creating Compatible Files
The file format of vector image files on Pebble is the PDC (Pebble Draw Command)
format, which includes all the instructions necessary to allow drawing of
vectors. These files are created from compatible SVG (Scalar Vector Graphics)
files using the
svg2pdc
tool.
To convert an SVG file to a PDC image of the same name:
$ python svg2pdc.py image.svg
To create a PDCS (Pebble Draw Command Sequence) from individual SVG frames,
specify the directory containing the frames with the --sequence
flag when
running svg2pdc
:
$ ls frames/
1.svg 2.svg 3.svg
4.svg 5.svg
$ python svg2pdc.py --sequence frames/
In the example above, this will create an output file in the frames
directory
called frames.pdc
that contains draw command data for the complete animation.
The svg2pdc
tool currently supports SVG files that use only the following
elements: g
, layer
, path
, rect
, polyline
, polygon
, line
, circle
.
We recommend using Adobe Illustrator to create compatible SVG icons and images. {% endmarkdown %}
For simplicity, compatible image and sequence files will be provided for you to use in your own project.
PDC icons
Example PDC image files are available for the icons listed in App Assets. These are ideal for use in many common types of apps, such as notification or weather apps.
[Download PDC icon files >{center,bg-lightblue,fg-white}]({{ site.links.s3_assets }}/assets/other/pebble-timeline-icons-pdc.zip)
Getting Started
^CP^ Begin a new [CloudPebble]({{ site.links.cloudpebble }}) project using the
blank template and add code only to push an initial Window
, such as the
example below:
^LC^ Begin a new project using pebble new-project
and create a simple app that
pushes a blank Window
, such as the example below:
#include <pebble.h>
static Window *s_main_window;
static void main_window_load(Window *window) {
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
}
static void main_window_unload(Window *window) {
}
static void init() {
s_main_window = window_create();
window_set_window_handlers(s_main_window, (WindowHandlers) {
.load = main_window_load,
.unload = main_window_unload,
});
window_stack_push(s_main_window, true);
}
static void deinit() {
window_destroy(s_main_window);
}
int main() {
init();
app_event_loop();
deinit();
}
Drawing a PDC Image
For this tutorial, use the example
weather_image.pdc
file provided.
^CP^ Add the PDC file as a project resource using the 'Add new' under
'Resources' on the left-hand side of the CloudPebble editor, with an
'Identifier' of WEATHER_IMAGE
, and a type of 'raw binary blob'. The file is
assumed to be called weather_image.pdc
.
^LC^ Add the PDC file to your project resources in package.json
as shown
below. Set the 'name' field to WEATHER_IMAGE
, and the 'type' field to raw
.
The file is assumed to be called weather_image.pdc
:
^LC^ Drawing a Pebble Draw Command image is just as simple as drawing a normal PNG
image to a graphics context, requiring only one draw call. First, load the
.pdc
file from resources, for example with the name
defined as
WEATHER_IMAGE
, as shown below.
^CP^ Drawing a Pebble Draw Command image is just as simple as drawing a normal
PNG image to a graphics context, requiring only one draw call. First, load the
.pdc
file from resources, for example with the 'Identifier' defined as
WEATHER_IMAGE
. This will be available in code as RESOURCE_ID_WEATHER_IMAGE
,
as shown below.
Declare a pointer of type GDrawCommandImage
at the top of the file:
static GDrawCommandImage *s_command_image;
Create and assign the GDrawCommandImage
in init()
, before calling
window_stack_push()
:
static void init() {
/* ... */
// Create the object from resource file
s_command_image = gdraw_command_image_create_with_resource(RESOURCE_ID_WEATHER_IMAGE);
/* ... */
}
Next, define the LayerUpdateProc
that will be used to draw the PDC image:
static void update_proc(Layer *layer, GContext *ctx) {
// Set the origin offset from the context for drawing the image
GPoint origin = GPoint(10, 20);
// Draw the GDrawCommandImage to the GContext
gdraw_command_image_draw(ctx, s_command_image, origin);
}
Next, create a Layer
to display the image:
static Layer *s_canvas_layer;
Next, set the LayerUpdateProc
that will do the rendering and add it to the
desired Window
:
static void main_window_load(Window *window) {
/* ... */
// Create the canvas Layer
s_canvas_layer = layer_create(GRect(30, 30, bounds.size.w, bounds.size.h));
// Set the LayerUpdateProc
layer_set_update_proc(s_canvas_layer, update_proc);
// Add to parent Window
layer_add_child(window_layer, s_canvas_layer);
}
Finally, don't forget to free the memory used by the Window
's sub-components
in main_window_unload()
:
static void main_window_unload(Window *window) {
layer_destroy(s_canvas_layer);
gdraw_command_image_destroy(s_command_image);
}
When run, the PDC image will be loaded, and rendered in the LayerUpdateProc
.
To put the image into contrast, we will finally change the Window
background
color after window_create()
:
window_set_background_color(s_main_window, GColorBlueMoon);
The result will look similar to the example shown below.
Playing a PDC Sequence
The GDrawCommandSequence
API allows developers to use vector graphics as
individual frames in a larger animation. Just like GDrawCommandImage
s, each
GDrawCommandFrame
is drawn to a graphics context in a LayerUpdateProc
.
For this tutorial, use the example
clock_sequence.pdc
file provided.
Begin a new app, with a C file containing the template provided above.
^CP^ Next, add the file as a raw
resource in the same way as for a PDC image,
for example with an Identifier
specified as CLOCK_SEQUENCE
.
^LC^ Next, add the file as a raw
resource in the same way as for a PDC image,
for example with the name
field specified in package.json
as
CLOCK_SEQUENCE
.
Load the PDCS in your app by first declaring a GDrawCommandSequence
pointer:
static GDrawCommandSequence *s_command_seq;
Next, initialize the object in init()
before calling window_stack_push()
:
static void init() {
/* ... */
// Load the sequence
s_command_seq = gdraw_command_sequence_create_with_resource(RESOURCE_ID_CLOCK_SEQUENCE);
/* ... */
}
Get the next frame and draw it in the LayerUpdateProc
. Then register a timer
to draw the next frame:
// Milliseconds between frames
#define DELTA 13
static int s_index = 0;
/* ... */
static void next_frame_handler(void *context) {
// Draw the next frame
layer_mark_dirty(s_canvas_layer);
// Continue the sequence
app_timer_register(DELTA, next_frame_handler, NULL);
}
static void update_proc(Layer *layer, GContext *ctx) {
// Get the next frame
GDrawCommandFrame *frame = gdraw_command_sequence_get_frame_by_index(s_command_seq, s_index);
// If another frame was found, draw it
if (frame) {
gdraw_command_frame_draw(ctx, s_command_seq, frame, GPoint(0, 30));
}
// Advance to the next frame, wrapping if neccessary
int num_frames = gdraw_command_sequence_get_num_frames(s_command_seq);
s_index++;
if (s_index == num_frames) {
s_index = 0;
}
}
Next, create a new Layer
to utilize the LayerUpdateProc
and add it to the
desired Window
.
Create the Window
pointer:
static Layer *s_canvas_layer;
Next, create the Layer
and assign it to the new pointer. Set its update
procedure and add it to the Window
:
static void main_window_load(Window *window) {
// Get Window information
Layer *window_layer = window_get_root_layer(window);
GRect bounds = layer_get_bounds(window_layer);
// Create the canvas Layer
s_canvas_layer = layer_create(GRect(30, 30, bounds.size.w, bounds.size.h));
// Set the LayerUpdateProc
layer_set_update_proc(s_canvas_layer, update_proc);
// Add to parent Window
layer_add_child(window_layer, s_canvas_layer);
}
Start the animation loop using a timer at the end of initialization:
// Start the animation
app_timer_register(DELTA, next_frame_handler, NULL);
Finally, remember to destroy the GDrawCommandSequence
and Layer
in
main_window_unload()
:
static void main_window_unload(Window *window) {
layer_destroy(s_canvas_layer);
gdraw_command_sequence_destroy(s_command_seq);
}
When run, the animation will be played by the timer at a framerate dictated by
DELTA
, looking similar to the example shown below:
What's Next?
You have now learned how to add vector images and animations to your apps.
Complete examples for these APIs are available under the pebble-examples
GitHub organization:
-
pdc-image
- Example implementation of a Pebble Draw Command Image. -
pdc-sequence
- Example implementation of a Pebble Draw Command Sequence animated icon.
More advanced tutorials will be added here in the future, so keep checking back!