mirror of
https://github.com/google/pebble.git
synced 2025-03-21 11:21:21 +00:00
240 lines
9.7 KiB
Markdown
240 lines
9.7 KiB
Markdown
|
---
|
||
|
# Copyright 2025 Google LLC
|
||
|
#
|
||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
# you may not use this file except in compliance with the License.
|
||
|
# You may obtain a copy of the License at
|
||
|
#
|
||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||
|
#
|
||
|
# Unless required by applicable law or agreed to in writing, software
|
||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
# See the License for the specific language governing permissions and
|
||
|
# limitations under the License.
|
||
|
|
||
|
title: Bezier Curves and GPaths on Pebble
|
||
|
author: lukasz
|
||
|
tags:
|
||
|
- Beautiful Code
|
||
|
date: 2015-02-13
|
||
|
summary: |
|
||
|
A look at how to use Bezier curves and GPaths to efficiently draw complex
|
||
|
paths in your Pebble apps and watchfaces.
|
||
|
banner: /images/blog/bezier-banner.png
|
||
|
---
|
||
|
|
||
|
Drawing complex paths requires a lot of manual work on Pebble. Here I'll show
|
||
|
you how to do this efficiently and quickly using a Pebble-optimized ``GPath``
|
||
|
algorithm and Bezier curves.
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
## Why Bezier Curves?
|
||
|
|
||
|
Typically, when a developer wants to create curve-based graphics on Pebble he
|
||
|
has to use prepared bitmaps or complex GPaths (or generate paths with
|
||
|
[svg2gpath](http://gpathsvg.org/)). Both of those approaches have serious
|
||
|
downsides: bitmaps require resource space to store, memory to process and
|
||
|
rotating bitmaps is a complex operation. Detailed GPaths, while fast in
|
||
|
processing, can take a long time to design without special tools.
|
||
|
|
||
|
Bezier curves are a very interesting solution, which may seem computationally
|
||
|
excessive but solves the obstacles of both the previous methods. Because they're
|
||
|
so simple to use they are common elements in many graphic systems, primarly
|
||
|
vector based graphics.
|
||
|
|
||
|
## Drawing Bezier Curves
|
||
|
|
||
|
The mathematical formula for a Bezier curve can be found on
|
||
|
[Wikipedia](http://en.wikipedia.org/wiki/Composite_B%C3%A9zier_curve). We can
|
||
|
fairly easily turn it into C code, but we have to make a couple of important
|
||
|
decisions. The formula itself is linear and requires us to decide how many steps
|
||
|
we want to take in order to draw our Bezier curve. It's safe to assume that
|
||
|
calculating 1000 points on our Bezier curve should be enough to display a
|
||
|
continuous line on the small Pebbles screen.
|
||
|
|
||
|
```c
|
||
|
void naive_bezier(GContext *ctx, GPoint points[]) {
|
||
|
for (double t = 0.0; t<1.0; t += 0.001) {
|
||
|
double tx = pow_d(1-t, 3) * points[0].x + 3 * t * pow_d(1-t, 2) * points[1].x +
|
||
|
3 * pow_d(t, 2) * (1-t) * points[3].x + pow_d (t, 3) * points[2].x;
|
||
|
double ty = pow_d(1-t, 3) * points[0].y + 3 * t * pow_d(1-t, 2) * points[1].y +
|
||
|
3 * pow_d(t, 2) * (1-t) * points[3].y + pow_d(t, 3) * points[2].y;
|
||
|
|
||
|
graphics_draw_pixel(ctx, GPoint(tx, ty));
|
||
|
}
|
||
|
}
|
||
|
```
|
||
|
|
||
|
**Notes:**
|
||
|
|
||
|
* `pow_d(double a, int b)` is simply a function which returns `b` to the power
|
||
|
of `a`.
|
||
|
|
||
|
* `points` contains array of 4 points used as parameters for drawing the Bezier
|
||
|
curve.
|
||
|
|
||
|
While this approach gives us precise results its also very computationally
|
||
|
expensive. Most of the work done by the CPU is redundant as most of those points
|
||
|
overlap each other due to the low screen resolution.
|
||
|
|
||
|
## Optimizing for Pebble
|
||
|
|
||
|
The simplest optimization would be reducing the number of steps and drawing
|
||
|
lines between computed points. While this will significantly reduce calculation
|
||
|
time, it will also make the curve less precise and pleasing to the eye. But what
|
||
|
if we could dynamically calculate the number of segments needed to create
|
||
|
perfectly smooth curve? Here's a method published by Maxim Shermanev which you
|
||
|
can find out more about on his website:
|
||
|
[www.antigrain.com](http://www.antigrain.com/research/adaptive_bezier/index.html).
|
||
|
|
||
|
Below you can see code from his research adapted to work efficiently on the
|
||
|
Pebble architecture:
|
||
|
|
||
|
```c
|
||
|
void recursive_bezier_fixed(int x1, int y1,
|
||
|
int x2, int y2,
|
||
|
int x3, int y3,
|
||
|
int x4, int y4){
|
||
|
// Calculate all the mid-points of the line segments
|
||
|
int x12 = (x1 + x2) / 2;
|
||
|
int y12 = (y1 + y2) / 2;
|
||
|
int x23 = (x2 + x3) / 2;
|
||
|
int y23 = (y2 + y3) / 2;
|
||
|
int x34 = (x3 + x4) / 2;
|
||
|
int y34 = (y3 + y4) / 2;
|
||
|
int x123 = (x12 + x23) / 2;
|
||
|
int y123 = (y12 + y23) / 2;
|
||
|
int x234 = (x23 + x34) / 2;
|
||
|
int y234 = (y23 + y34) / 2;
|
||
|
int x1234 = (x123 + x234) / 2;
|
||
|
int y1234 = (y123 + y234) / 2;
|
||
|
|
||
|
// Angle Condition
|
||
|
int32_t a23 = atan2_lookup((y3 - y2) / fixedpoint_base, (x3 - x2) / fixedpoint_base);
|
||
|
int32_t da1 = abs(a23 - atan2_lookup((y2 - y1) / fixedpoint_base, (x2 - x1) / fixedpoint_base));
|
||
|
int32_t da2 = abs(atan2_lookup((y4 - y3) / fixedpoint_base, (x4 - x3) / fixedpoint_base) - a23);
|
||
|
if(da1 >= TRIG_MAX_ANGLE) da1 = TRIG_MAX_ANGLE - da1;
|
||
|
if(da2 >= TRIG_MAX_ANGLE) da2 = TRIG_MAX_ANGLE - da2;
|
||
|
|
||
|
if(da1 + da2 < m_angle_tolerance)
|
||
|
{
|
||
|
// Finally we can stop the recursion
|
||
|
add_point(x1234 / fixedpoint_base, y1234 / fixedpoint_base);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Continue subdivision
|
||
|
recursive_bezier_fixed(x1, y1, x12, y12, x123, y123, x1234, y1234);
|
||
|
recursive_bezier_fixed(x1234, y1234, x234, y234, x34, y34, x4, y4);
|
||
|
}
|
||
|
|
||
|
bool bezier_fixed(GPathBuilder *builder, GPoint p1, GPoint p2, GPoint p3, GPoint p4) {
|
||
|
// Translate points to fixedpoint realms
|
||
|
int32_t x1 = p1.x * fixedpoint_base;
|
||
|
int32_t x2 = p2.x * fixedpoint_base;
|
||
|
int32_t x3 = p3.x * fixedpoint_base;
|
||
|
int32_t x4 = p4.x * fixedpoint_base;
|
||
|
int32_t y1 = p1.y * fixedpoint_base;
|
||
|
int32_t y2 = p2.y * fixedpoint_base;
|
||
|
int32_t y3 = p3.y * fixedpoint_base;
|
||
|
int32_t y4 = p4.y * fixedpoint_base;
|
||
|
|
||
|
if (recursive_bezier_fixed(builder, x1, y1, x2, y2, x3, y3, x4, y4)) {
|
||
|
return gpath_builder_line_to_point(builder, p4);
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
```
|
||
|
|
||
|
**Notes:**
|
||
|
|
||
|
* This code uses fixedpoint integers since the Pebble CPU doesn't support
|
||
|
floating point operations. You can find more about that in a talk given by
|
||
|
Matthew Hungerford during 2014 Pebble Developer Retreat (video available
|
||
|
[here](https://www.youtube.com/watch?v=8tOhdUXcSkw)).
|
||
|
|
||
|
* To determine the angle, the algorithm calculates parameters of the curve at
|
||
|
given points. This implementation is very effective since Pebble's
|
||
|
`atan2_lookup` is just looking up that value in a precomputed table.
|
||
|
|
||
|
* `m_angle_tolerance` is the angle of the curve we're looking for, expressed in
|
||
|
degrees. In our case it's 10 degrees: `int32_t m_angle_tolerance =
|
||
|
(TRIG_MAX_ANGLE / 360) * 10;`
|
||
|
|
||
|
## Applying Code to GPath
|
||
|
|
||
|
In order to make it easy for developers, we have prepared the GPathBuilder
|
||
|
library which will ease the process of creating GPaths out of a few Bezier
|
||
|
curves and/or lines. The resulting path can already be manipulated with the
|
||
|
[existing APIs](/docs/c/group___path_drawing.html#ga1ba79344b9a34432a44af09bed8b00fd). You can find it on the
|
||
|
[pebble-hacks Github page](https://github.com/pebble-hacks/gpath-bezier) along
|
||
|
with a simple demo app.
|
||
|
|
||
|
Usage is extremely simple. Here are the main functions in the API:
|
||
|
|
||
|
- `GPathBuilder* gpath_builder_create(uint32_t max_points)` will create the
|
||
|
GPathBuilder object you will need to use in order to create a GPath with
|
||
|
Bezier curves. `max_points` sets the limit on number of points created in the
|
||
|
process.
|
||
|
- `void gpath_builder_destroy(GPathBuilder *builder)` will destroy the
|
||
|
GPathBuilder object and free the memory it used.
|
||
|
- `bool gpath_builder_line_to_point(GPathBuilder *builder, GPoint to_point)`
|
||
|
will create a straight line from last point to the given point.
|
||
|
- `bool gpath_builder_curve_to_point(GPathBuilder *builder, GPoint to_point,
|
||
|
GPoint control_point_1, GPoint control_point_2)` will create a Bezier curve
|
||
|
from the last point to a given point (`to_point`), `control_point_1` and
|
||
|
`control_point_2` are used as parameters for the Bezier curve for last point
|
||
|
and given point respectively.
|
||
|
- `GPath* gpath_builder_create_path(GPathBuilder *builder)` will return a GPath
|
||
|
object ready to be used in your graphic routine. Remember that the
|
||
|
GPathBuilder is not being destroyed in the process and you have to do that
|
||
|
manually.
|
||
|
|
||
|
Below is shown a simple shape involving two curves and the code required to
|
||
|
create the path:
|
||
|
|
||
|

|
||
|
|
||
|
```c
|
||
|
// Create GPathBuilder object
|
||
|
GPathBuilder *builder = gpath_builder_create(MAX_POINTS);
|
||
|
|
||
|
// Move to the starting point of the GPath
|
||
|
gpath_builder_move_to_point(builder, GPoint(0, -60));
|
||
|
// Create curve
|
||
|
gpath_builder_curve_to_point(builder, GPoint(60, 0), GPoint(35, -60), GPoint(60, -35));
|
||
|
// Create straight line
|
||
|
gpath_builder_line_to_point(builder, GPoint(-60, 0));
|
||
|
// Create another curve
|
||
|
gpath_builder_curve_to_point(builder, GPoint(0, 60), GPoint(-60, 35), GPoint(-35, 60));
|
||
|
// Create another straight line
|
||
|
gpath_builder_line_to_point(builder, GPoint(0, -60));
|
||
|
|
||
|
// Create GPath object out of our GPathBuilder object
|
||
|
s_path = gpath_builder_create_path(builder);
|
||
|
// Destroy GPathBuilder object
|
||
|
gpath_builder_destroy(builder);
|
||
|
|
||
|
// Get window bounds
|
||
|
GRect bounds = layer_get_bounds(window_get_root_layer(window));
|
||
|
// Move newly created GPath to the center of the screen
|
||
|
gpath_move_to(s_path, GPoint((int16_t)(bounds.size.w/2), (int16_t)(bounds.size.h/2)));
|
||
|
```
|
||
|
|
||
|
And there you have it, complex GPaths built with a few lines of code.
|
||
|
|
||
|
## Conclusion
|
||
|
|
||
|
This library should make it easier to create amazing graphics on Pebble and can
|
||
|
be used in animations as it's lightweight and fast. Bear in mind that the
|
||
|
GPathBuilder temporarily uses RAM proportinal to `max_points` until
|
||
|
`gpath_builder_destroy` is called. You can find the example app and library code
|
||
|
on the [pebble-hacks Github page](https://github.com/pebble-hacks/gpath-bezier).
|
||
|
|
||
|
We are also looking forward to this technology being used in online tools such
|
||
|
as [SVG to Pebble GPath Converter](http://gpathsvg.org/) by awesome Pebble
|
||
|
developer [Rajendra Serber](https://github.com/ardnejar).
|