pebble/src/fw/applib/graphics/graphics_circle.c
Josh Soref c1e8ef32bd spelling: variable
Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2025-01-29 00:03:29 -05:00

1489 lines
58 KiB
C

/*
* Copyright 2024 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.
*/
#include "graphics_circle.h"
#include "graphics_circle_private.h"
#include "graphics.h"
#include "graphics_private.h"
#include "kernel/pbl_malloc.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/size.h"
#include "util/trig.h"
static Fixed_S16_3 prv_get_circle_border(int16_t y, uint16_t radius) {
// Match to the precision we need here
y *= FIXED_S16_3_ONE.raw_value;
radius *= FIXED_S16_3_ONE.raw_value;
return (Fixed_S16_3){.raw_value = radius - integer_sqrt(radius * radius - y * y)};
}
static Fixed_S16_3 prv_get_ellipsis_border(Fixed_S16_3 offset, uint32_t offset_radius_sq,
uint32_t opposite_radius_sq) {
if (offset_radius_sq == opposite_radius_sq) {
// We're dealing with a circle
return (Fixed_S16_3){.raw_value = integer_sqrt((offset_radius_sq << FIXED_S16_3_PRECISION) -
offset.raw_value * offset.raw_value)};
}
return (Fixed_S16_3){
.raw_value = (integer_sqrt((opposite_radius_sq - opposite_radius_sq *
((offset.raw_value * offset.raw_value) >> FIXED_S16_3_PRECISION) /
offset_radius_sq) << FIXED_S16_3_PRECISION))
};
}
static GPointPrecise prv_get_rotated_precise_point(GPointPrecise center, uint16_t radius,
uint32_t angle) {
return GPointPrecise((center.x.raw_value + radius * sin_lookup(angle) / TRIG_MAX_RATIO),
(center.y.raw_value - radius * cos_lookup(angle) / TRIG_MAX_RATIO));
}
static GPointPrecise prv_get_rotated_precise_point_for_ellipsis(GPointPrecise center,
uint16_t radius_x,
uint16_t radius_y,
uint32_t angle) {
// PBL-25637: goes bonkers for ellipsis and angles around 90° and 270°
if (radius_x == radius_y) {
// We're dealing with circle here - theres an easier way...
return prv_get_rotated_precise_point(center, radius_x, angle);
}
// This is an edge case due to fixedpoint math
if (angle % QUADRANT_ANGLE == 0) {
radius_x <<= FIXED_S16_3_PRECISION;
radius_y <<= FIXED_S16_3_PRECISION;
switch (angle / QUADRANT_ANGLE) {
case 0:
return GPointPrecise(center.x.raw_value, center.y.raw_value - radius_y);
case 1:
return GPointPrecise(center.x.raw_value + radius_x, center.y.raw_value);
case 2:
return GPointPrecise(center.x.raw_value, center.y.raw_value + radius_y);
default:
return GPointPrecise(center.x.raw_value - radius_x, center.y.raw_value);
}
}
// This algorithm operates on angle starting at our 90° mark, so we add 90°
// and flip x/y coordinates (see last line of this function)
angle = (angle + (TRIG_MAX_ANGLE / 4)) % TRIG_MAX_ANGLE;
// This is going to be divided by fixed-point precision number so division here is unnecessary
int32_t radius_xy = ((int32_t)radius_x * radius_y);
int32_t radius_xx = ((int32_t)radius_x * radius_x) >> FIXED_S16_3_PRECISION;
int32_t radius_yy = ((int32_t)radius_y * radius_y) >> FIXED_S16_3_PRECISION;
int64_t sin = sin_lookup(angle);
int64_t cos = cos_lookup(angle);
int64_t sin_sq = sin * sin / TRIG_MAX_RATIO;
int64_t cos_sq = cos * cos / TRIG_MAX_RATIO;
// We simulate tan(angle) by sin(angle)/cos(angle)
int64_t rx_tan = radius_xx * sin_sq;
if (cos_sq != 0) {
rx_tan /= cos_sq;
}
int32_t sqrt_x = integer_sqrt((radius_yy + rx_tan) << FIXED_S16_3_PRECISION);
int16_t x = 0;
if (sqrt_x > 0) {
x = radius_xy / sqrt_x;
}
// Between 90° and 270° we flip the x
if (angle >= (TRIG_MAX_ANGLE / 4) && angle < (TRIG_MAX_ANGLE * 3 / 4)) {
x *= -1;
}
// And y in this case is just x multiplied by tan(angle)
int16_t y = x * sin / cos;
// Flipping results by center point
return GPointPrecise(center.x.raw_value - x, center.y.raw_value - y);
}
T_STATIC void graphics_circle_quadrant_draw_1px_non_aa(GContext* ctx, GPoint p, uint16_t radius,
GCornerMask quadrant) {
int f = 1 - radius;
int ddF_x = 1;
int ddF_y = -2 * radius;
int x = 0;
uint16_t y = radius;
p.x += ctx->draw_state.drawing_box.origin.x;
p.y += ctx->draw_state.drawing_box.origin.y;
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
if (quadrant & GCornerBottomRight) {
graphics_private_set_pixel(ctx, GPoint(p.x + x, p.y + y));
graphics_private_set_pixel(ctx, GPoint(p.x + y, p.y + x));
}
if (quadrant & GCornerTopRight) {
graphics_private_set_pixel(ctx, GPoint(p.x + x, p.y - y));
graphics_private_set_pixel(ctx, GPoint(p.x + y, p.y - x));
}
if (quadrant & GCornerBottomLeft) {
graphics_private_set_pixel(ctx, GPoint(p.x - x, p.y + y));
graphics_private_set_pixel(ctx, GPoint(p.x - y, p.y + x));
}
if (quadrant & GCornerTopLeft) {
graphics_private_set_pixel(ctx, GPoint(p.x - x, p.y - y));
graphics_private_set_pixel(ctx, GPoint(p.x - y, p.y - x));
}
}
}
static void prv_plot4(GBitmap *fb, GRect *clip_box, GPoint center, GPoint offset, int8_t brightness,
GColor stroke_color, GCornerMask quadrant) {
/*
* This will mirror given offset point over x and y coordinates of given center point
* |
* x1 | x
* |
* |
* -------+--------
* |
* |
* x2 | x3
* |
*
* + center point
* - x coordinate mirror line
* | y coordinate mirror line
* x given offset point
* xn mirrored points
*/
for (unsigned int i=0; i < ARRAY_LENGTH(quadrant_mask_mul); i++) {
if (quadrant_mask_mul[i].mask & quadrant) {
int16_t x = center.x + (offset.x * quadrant_mask_mul[i].x_mul);
int16_t y = center.y + (offset.y * quadrant_mask_mul[i].y_mul);
graphics_private_plot_pixel(fb, clip_box, x, y, brightness, stroke_color);
}
}
}
static void prv_plot8(GBitmap *fb, GRect *clip_box, GPoint center, GPoint offset, int8_t brightness,
GColor stroke_color, GCornerMask quadrant) {
/*
* This will mirror given offset point over all eighths of the circle at given center
* \ x8| x /
* \ | /
* \ | /
* x7 \ | / x2
* -------+--------
* x6 / | \ x3
* / | \
* / | \
* / x5| x4 \
*
* + center point
* - x coordinate mirror line
* | y coordinate mirror line
* / 45 degree mirror line
* \ 135 degree mirror line
* x given offset point
* xn mirrored points
*/
prv_plot4(fb, clip_box, center, offset, brightness, stroke_color, quadrant);
// Swapping x and y for rest of the circle
prv_plot4(fb, clip_box, center, GPoint(offset.y, offset.x), brightness, stroke_color, quadrant);
}
#if PBL_COLOR
T_STATIC void graphics_circle_quadrant_draw_1px_aa(GContext* ctx, GPoint p, uint16_t radius,
GCornerMask quadrant) {
/* This will draw antialiased circle with width of 1px, can be drawn in quadrants
* Based on wu-xiang line drawing, will draw circle in two steps
* 1. Calculate point on the edge of eighth of the circle and plot it around by mirroring
* - if point is matching pixel perfectly thats going to be on fully colored pixel
* - if theres fraction, two pixels will be colored accordingly
* 2. Fill special case pixels (pixels that are between mirrored eighths)
* - special code to avoid overdrawing of those pixels and preserve antialiasing
* - three pixels calculated for circle with radius > 6
* - two pixels calculated for circle with radius < 6
*
* Theres also special case for the radius of 3, where algorithm couldnt stop at right
* point and wasn't drawing two pixels on each quadrant
*
* Here's quadrant example:
*
* 45 degree angle (radius * (sqrt(2)/2))
* |
* v
*
* | | | |
* | | |
* | o
* o o <- 45 degree angle (radius * (sqrt(2)/2))
* - -
* - -
* - -
* -
* x -
*
* | original calculated pixels for plotting
* - mirrored eight of the circle (will mirror more of them if necessary)
* o special case pixels
* x center of the circle
*/
// To match whats being drawn by non-aa graphics_draw_circle...
radius++;
// Apply drawing_box
p.x += ctx->draw_state.drawing_box.origin.x;
p.y += ctx->draw_state.drawing_box.origin.y;
// As close to sqrt(2)/2 as possible
int stop_progress = radius * 707 / 1000;
int progress;
uint16_t radius_fixed = radius * FIXED_S16_3_ONE.raw_value;
int16_t weighting_compliment_mask = MAX_PLOT_BRIGHTNESS;
int16_t weighting = 0;
// Locking framebuffer...
GBitmap *framebuffer = graphics_capture_frame_buffer(ctx);
GColor stroke_color = ctx->draw_state.stroke_color;
// Step 1
for (progress = 0; progress < stop_progress; progress++) {
Fixed_S16_3 edge = (Fixed_S16_3){.raw_value = radius_fixed -
prv_get_circle_border(progress, radius).raw_value};
if (edge.integer != 0) {
weighting = (edge.fraction >> 1);
prv_plot8(framebuffer, &ctx->draw_state.clip_box, p, GPoint(edge.integer - 1, progress),
weighting, stroke_color, quadrant);
prv_plot8(framebuffer, &ctx->draw_state.clip_box, p, GPoint(edge.integer, progress),
weighting ^ weighting_compliment_mask, stroke_color, quadrant);
} else {
prv_plot8(framebuffer, &ctx->draw_state.clip_box, p, GPoint(edge.integer, progress),
MAX_PLOT_OPACITY, stroke_color, quadrant);
}
}
// Behold for magic number 3!!!
// Note: magic numbers explained in main comment for this function
int special_case_pixels = 3;
// Accompanied by magic number 7 (not 6, we increased radius at beginning of this function)
if (radius < 7) {
// And sometimes magic number 2
special_case_pixels = 2;
}
// Step 2
// Special code for filling gap between mirrored parts in a manner that wont overdraw pixels
for (; progress < stop_progress + special_case_pixels; progress++) {
Fixed_S16_3 edge = (Fixed_S16_3){.raw_value = radius_fixed -
prv_get_circle_border(progress, radius).raw_value};
if (edge.integer != 0) {
weighting = (edge.fraction >> 1);
if (edge.integer - 1 > radius - stop_progress) {
prv_plot4(framebuffer, &ctx->draw_state.clip_box, p, GPoint(edge.integer - 1, progress),
weighting, stroke_color, quadrant);
}
if (edge.integer > radius - stop_progress) {
prv_plot4(framebuffer, &ctx->draw_state.clip_box, p, GPoint(edge.integer, progress),
weighting ^ weighting_compliment_mask, stroke_color, quadrant);
}
} else {
if (edge.integer > radius - stop_progress) {
prv_plot4(framebuffer, &ctx->draw_state.clip_box, p, GPoint(edge.integer, progress),
MAX_PLOT_OPACITY, stroke_color, quadrant);
}
}
}
// And for grand finale, super special case for radius of 4 (3 outside of this funct):
if (radius == 4) {
prv_plot4(framebuffer, &ctx->draw_state.clip_box, p, GPoint(2, 2), MAX_PLOT_OPACITY,
stroke_color, quadrant);
prv_plot4(framebuffer, &ctx->draw_state.clip_box, p, GPoint(2, 3), 2, stroke_color, quadrant);
}
// Releasing framebuffer...
graphics_release_frame_buffer(ctx, framebuffer);
}
#endif // PBL_COLOR
static void prv_circle_arc_draw_1px(GContext* ctx, GPoint center, uint16_t radius,
int32_t angle_start, int32_t angle_end) {
// TODO: PBL-23119 Write non-aa function
// TODO: PBL-24777 Write new 1px functions (with support for ellipsis)
// prv_circle_quadrant_part_1px_aa(ctx, center, radius, config.start_quadrant.angle_start,
// config.start_quadrant.angle_end);
//
// prv_circle_quadrant_part_1px_aa(ctx, center, radius, config.end_quadrant.angle_start,
// config.end_quadrant.angle_end);
//
// if (config.full_quadrants != GCornerNone) {
// graphics_circle_quadrant_draw_1px_aa(ctx, center, radius, config.full_quadrants);
// }
}
inline void prv_vline_quadrant(GCornerMask quadrant, GCornerMask desired, GContext *ctx, int16_t x,
Fixed_S16_3 start, Fixed_S16_3 end) {
if (quadrant & desired) {
graphics_private_draw_vertical_line(ctx, x, start, end);
}
}
inline void prv_hline_quadrant(GCornerMask quadrant, GCornerMask desired, GContext *ctx, int16_t y,
Fixed_S16_3 start, Fixed_S16_3 end) {
if (quadrant & desired) {
graphics_private_draw_horizontal_line(ctx, y, start, end);
}
}
static void prv_stroke_circle_quadrant_full(GContext* ctx, GPoint p, uint16_t radius,
uint8_t stroke_width, GCornerMask quadrant) {
// This algorithm will draw stroked circle with variable width (only odd numbers for now)
const uint8_t half_stroke_width = stroke_width / 2;
const int16_t inner_radius = radius - half_stroke_width;
const uint8_t outer_radius = radius + half_stroke_width;
if (inner_radius < 1) {
// Hack for filling circles: filling is done by line primitives using stroke_color by default
GColor temp_color = ctx->draw_state.fill_color;
ctx->draw_state.fill_color = ctx->draw_state.stroke_color;
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
graphics_internal_circle_quadrant_fill_aa(ctx, p, outer_radius, quadrant);
} else {
graphics_circle_quadrant_fill_non_aa(ctx, p, outer_radius, quadrant);
}
#else
graphics_circle_quadrant_fill_non_aa(ctx, p, outer_radius, quadrant);
#endif
// Restore original status
ctx->draw_state.fill_color = temp_color;
return;
}
// Since fill_oval will use fill color, we have to swap those:
const GColor fill_color = ctx->draw_state.fill_color;
ctx->draw_state.fill_color = ctx->draw_state.stroke_color;
// For pixel matching we need to decrease inner radius...
prv_fill_oval_quadrant(ctx, p, outer_radius, outer_radius,
inner_radius - 1, inner_radius - 1, quadrant);
ctx->draw_state.fill_color = fill_color;
}
static void prv_stroke_circle_quadrant_full_override_aa(GContext* ctx, GPoint p, uint16_t radius,
uint8_t stroke_width, GCornerMask quadrant,
bool anti_aliased) {
#if PBL_COLOR
// Force antialiasing setting
bool temp_anti_aliased = ctx->draw_state.antialiased;
ctx->draw_state.antialiased = anti_aliased;
#endif
// Call stroke circle quadrant function
prv_stroke_circle_quadrant_full(ctx, p, radius, stroke_width, quadrant);
#if PBL_COLOR
// Restore previous antialiasing setting
ctx->draw_state.antialiased = temp_anti_aliased;
#endif
}
#if PBL_COLOR
//! Draws anit-aliased stroked quadrant of a circle
//! @internal
T_STATIC void graphics_circle_quadrant_draw_stroked_aa(GContext* ctx, GPoint p, uint16_t radius,
uint8_t stroke_width, GCornerMask quadrant) {
prv_stroke_circle_quadrant_full_override_aa(ctx, p, radius, stroke_width, quadrant, true);
}
#endif // PBL_COLOR
//! Draws aliased stroked quadrant of a circle
//! @internal
T_STATIC void graphics_circle_quadrant_draw_stroked_non_aa(GContext* ctx, GPoint p, uint16_t radius,
uint8_t stroke_width,
GCornerMask quadrant) {
prv_stroke_circle_quadrant_full_override_aa(ctx, p, radius, stroke_width, quadrant, false);
}
void graphics_circle_quadrant_draw(GContext* ctx, GPoint p, uint16_t radius, GCornerMask quadrant) {
uint8_t stroke_width = ctx->draw_state.stroke_width;
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
if (stroke_width > 1) {
// Antialiased and Stroke Width > 1
graphics_circle_quadrant_draw_stroked_aa(ctx, p, radius, stroke_width, quadrant);
return;
} else {
// Antialiased and Stroke Width == 1 (not supported on 1-bit color)
graphics_circle_quadrant_draw_1px_aa(ctx, p, radius, quadrant);
return;
}
}
#endif
if (stroke_width > 1) {
// Non-Antialiased and Stroke Width > 1
graphics_circle_quadrant_draw_stroked_non_aa(ctx, p, radius, stroke_width, quadrant);
} else {
// Non-Antialiased and Stroke Width == 1
graphics_circle_quadrant_draw_1px_non_aa(ctx, p, radius, quadrant);
}
}
T_STATIC void graphics_circle_draw_1px_non_aa(GContext* ctx, GPoint p, uint16_t radius) {
graphics_circle_quadrant_draw_1px_non_aa(ctx, p, radius, GCornersAll);
p.x += ctx->draw_state.drawing_box.origin.x;
p.y += ctx->draw_state.drawing_box.origin.y;
graphics_private_set_pixel(ctx, GPoint(p.x, p.y + radius));
graphics_private_set_pixel(ctx, GPoint(p.x, p.y - radius));
graphics_private_set_pixel(ctx, GPoint(p.x + radius, p.y));
graphics_private_set_pixel(ctx, GPoint(p.x - radius, p.y));
}
#if PBL_COLOR
T_STATIC void graphics_circle_draw_1px_aa(GContext* ctx, GPoint p, uint16_t radius) {
graphics_circle_quadrant_draw_1px_aa(ctx, p, radius, GCornersAll);
}
//! Draws an antialiased circle of stroke width > 1
//! @note This only supports odd numbers for stroke_width - even numbers will be rounded up.
//! Minimal supported stroke_width is 3
T_STATIC void graphics_circle_draw_stroked_aa(GContext* ctx, GPoint p, uint16_t radius,
uint8_t stroke_width) {
graphics_circle_quadrant_draw_stroked_aa(ctx, p, radius, stroke_width, GCornersAll);
}
#endif // PBL_COLOR
//! Draws a non-antialiased circle of stroke width > 1
//! @note This only supports odd numbers for stroke_width - even numbers will be rounded up.
//! Minimal supported stroke_width is 3
T_STATIC void graphics_circle_draw_stroked_non_aa(GContext* ctx, GPoint p, uint16_t radius,
uint8_t stroke_width) {
graphics_circle_quadrant_draw_stroked_non_aa(ctx, p, radius, stroke_width, GCornersAll);
}
// Lifted directly from: http://en.wikipedia.org/wiki/Midpoint_circle_algorithm
void graphics_draw_circle(GContext* ctx, GPoint p, uint16_t radius) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
if (radius == 0) {
// Special case radius 0 to fill a circle with radius equal to half the stroke width
// Backup the fill color and set that to the current stroke color since the fill color
// is what is used for fill circle. Restore the fill color afterwards.
GColor backup_fill_color = ctx->draw_state.fill_color;
ctx->draw_state.fill_color = ctx->draw_state.stroke_color;
graphics_fill_circle(ctx, p, ctx->draw_state.stroke_width / 2);
ctx->draw_state.fill_color = backup_fill_color;
return;
}
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
if (ctx->draw_state.stroke_width > 1) {
// Antialiased and Stroke Width > 1
graphics_circle_draw_stroked_aa(ctx, p, radius, ctx->draw_state.stroke_width);
return;
} else {
// Antialiased and Stroke Width == 1
graphics_circle_draw_1px_aa(ctx, p, radius);
return;
}
}
#endif
if (ctx->draw_state.stroke_width > 1) {
// Non-Antialiased and Stroke Width > 1
graphics_circle_draw_stroked_non_aa(ctx, p, radius, ctx->draw_state.stroke_width);
} else {
// Non-Antialiased and Stroke Width == 1
graphics_circle_draw_1px_non_aa(ctx, p, radius);
}
}
#if defined(PLATFORM_TINTIN)
NOINLINE // Save space on tintin
#else
ALWAYS_INLINE // Optimize for speed on other platforms
#endif
static void prv_fill_horizontal_line(GContext *ctx, GPoint p, int16_t width) {
graphics_fill_rect(ctx, &(GRect) { .origin = p, .size = { width, 1 } });
}
void graphics_circle_quadrant_fill_non_aa(GContext* ctx, GPoint p, uint16_t radius,
GCornerMask quadrant) {
const int16_t x0 = p.x;
const int16_t y0 = p.y;
int f = 1 - radius;
int ddF_x = 1;
int ddF_y = -2 * radius;
int x = 0;
uint16_t y = radius;
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
if (quadrant & GCornerBottomLeft) {
if (x == 1) {
prv_fill_horizontal_line(ctx, GPoint(x0 - radius, y0), radius + 1);
}
prv_fill_horizontal_line(ctx, GPoint(x0 - x, y0 + y), x + 1);
prv_fill_horizontal_line(ctx, GPoint(x0 - y, y0 + x), y + 1);
}
if (quadrant & GCornerBottomRight) {
if (x == 1) {
prv_fill_horizontal_line(ctx, GPoint(x0, y0), radius + 1);
}
prv_fill_horizontal_line(ctx, GPoint(x0, y0 + y), x + 1);
prv_fill_horizontal_line(ctx, GPoint(x0, y0 + x), y + 1);
}
if (quadrant & GCornerTopLeft) {
if (x == 1) {
prv_fill_horizontal_line(ctx, GPoint(x0 - radius, y0), radius + 1);
}
prv_fill_horizontal_line(ctx, GPoint(x0 - x, y0 - y), x + 1);
prv_fill_horizontal_line(ctx, GPoint(x0 - y, y0 - x), y + 1);
}
if (quadrant & GCornerTopRight) {
if (x == 1) {
prv_fill_horizontal_line(ctx, GPoint(x0, y0), radius + 1);
}
prv_fill_horizontal_line(ctx, GPoint(x0, y0 - y), x + 1);
prv_fill_horizontal_line(ctx, GPoint(x0, y0 - x), y + 1);
}
}
}
static void graphics_fill_half_circle(GContext* ctx, int x0, int y0, uint16_t radius,
uint8_t section) {
int f = 1 - radius;
int ddF_x = 1;
int ddF_y = -2 * radius;
int x = 0;
uint16_t y = radius;
while (x < y) {
if (f >= 0) {
y--;
ddF_y += 2;
f += ddF_y;
}
x++;
ddF_x += 2;
f += ddF_x;
// bottom
if (section & GCornersBottom) {
prv_fill_horizontal_line(ctx, GPoint(x0 - x, y0 + y), 2 * x + 1);
prv_fill_horizontal_line(ctx, GPoint(x0 - y, y0 + x), 2 * y + 1);
}
// top
if (section & GCornersTop) {
prv_fill_horizontal_line(ctx, GPoint(x0 - x, y0 - y), 2 * x + 1);
prv_fill_horizontal_line(ctx, GPoint(x0 - y, y0 - x), 2 * y + 1);
}
}
}
MOCKABLE void graphics_circle_fill_non_aa(GContext* ctx, GPoint p, uint16_t radius) {
prv_fill_horizontal_line(ctx, GPoint(p.x - radius, p.y), 2 * radius + 1);
graphics_fill_half_circle(ctx, p.x, p.y, radius, GCornersAll);
}
#if PBL_COLOR
MOCKABLE void graphics_internal_circle_quadrant_fill_aa(GContext* ctx, GPoint p, uint16_t radius,
GCornerMask quadrant) {
// Radius cannot be smaller than 1
PBL_ASSERTN(radius > 0);
prv_fill_oval_quadrant(ctx, p, radius, radius, 0, 0, quadrant);
}
#endif // PBL_COLOR
// Returns x for f(x) = g(x) with f(x)=y and a line g(x) that goes through two given points a,b
static int16_t prv_intersection_between_horizontal_and_line(Fixed_S16_3 progress, GPointPrecise top,
GPointPrecise bottom) {
if (bottom.y.raw_value - top.y.raw_value == 0) {
return top.x.raw_value + (bottom.x.raw_value - top.x.raw_value);
}
return top.x.raw_value + (bottom.x.raw_value - top.x.raw_value) *
(progress.raw_value - top.y.raw_value) /
(bottom.y.raw_value - top.y.raw_value);
}
static void prv_swap_precise_points(GPointPrecise *p0, GPointPrecise *p1) {
GPointPrecise tmp = *p0;
*p0 = *p1;
*p1 = tmp;
}
static void prv_draw_scanline_collision_points(GContext *ctx, int16_t y,
int16_t left, int16_t right,
int16_t starting_edge, int16_t ending_edge,
bool ignore_close_angles) {
if (starting_edge > ending_edge || (ignore_close_angles && starting_edge == ending_edge)) {
// Two separate drawings...
starting_edge = MAX(starting_edge, left);
ending_edge = MIN(ending_edge, right);
if (left <= ending_edge) {
graphics_private_draw_horizontal_line(ctx, y,
(Fixed_S16_3){.raw_value = left},
(Fixed_S16_3){.raw_value = ending_edge -
FIXED_S16_3_ONE.raw_value});
}
if (starting_edge <= right) {
graphics_private_draw_horizontal_line(ctx, y,
(Fixed_S16_3){.raw_value = starting_edge},
(Fixed_S16_3){.raw_value = right -
FIXED_S16_3_ONE.raw_value});
}
} else {
starting_edge = (MAX(left, starting_edge));
ending_edge = (MIN(right, ending_edge));
if (starting_edge <= ending_edge) {
graphics_private_draw_horizontal_line(ctx, y,
(Fixed_S16_3){.raw_value = starting_edge},
(Fixed_S16_3){.raw_value = ending_edge -
FIXED_S16_3_ONE.raw_value});
}
}
}
static GCornerMask prv_get_full_quadrants(int8_t starting_quadrant, int8_t ending_quadrant) {
GCornerMask quadrants_solid = GCornerNone;
if (starting_quadrant >= ending_quadrant) {
ending_quadrant += QUADRANTS_NUM;
}
for (int i = starting_quadrant + 1; i < ending_quadrant; i++) {
quadrants_solid = quadrants_solid | radius_quadrants[i % QUADRANTS_NUM];
}
return quadrants_solid;
}
static void prv_get_angles_mask_edge(Fixed_S16_3 y, GPointPrecise center, GCornerMask quadrant,
int16_t *top_edge, int16_t *bottom_edge,
GPointPrecise top, GPointPrecise bottom) {
// This function determines where scanline is in position to the angle line and sets
// angle masking values accordingly
if (quadrant & GCornersTop) {
if (center.y.raw_value - y.raw_value <= bottom.y.raw_value) {
if (center.y.raw_value - y.raw_value >= top.y.raw_value) {
*top_edge = prv_intersection_between_horizontal_and_line((Fixed_S16_3){.raw_value =
(center.y.raw_value - y.raw_value)}, top, bottom);
} else {
*top_edge = top.x.raw_value;
}
} else {
*top_edge = bottom.x.raw_value;
}
} else {
if (center.y.raw_value + y.raw_value >= top.y.raw_value) {
if (center.y.raw_value + y.raw_value <= bottom.y.raw_value) {
*bottom_edge = prv_intersection_between_horizontal_and_line((Fixed_S16_3){.raw_value =
(center.y.raw_value + y.raw_value)}, top, bottom);
} else {
*bottom_edge = bottom.x.raw_value;
}
} else {
*bottom_edge = top.x.raw_value;
}
}
}
T_STATIC EllipsisDrawConfig prv_calc_draw_config_ellipsis(int32_t angle_start, int32_t angle_end) {
PBL_ASSERTN(angle_start <= angle_end);
EllipsisDrawConfig config = (EllipsisDrawConfig){(EllipsisPartDrawConfig){0, GCornerNone},
GCornerNone,
(EllipsisPartDrawConfig){0, GCornerNone}};
// Nothing to draw case:
if (angle_end == angle_start) {
return config;
}
// Full circle case:
if (angle_end - angle_start >= TRIG_MAX_ANGLE) {
config.full_quadrants = GCornersAll;
return config;
}
int32_t angle_start_normalized = normalize_angle(angle_start);
int32_t angle_end_normalized = normalize_angle(angle_end);
int8_t starting_quadrant_normalized = (angle_start_normalized / QUADRANT_ANGLE) % 4;
int8_t ending_quadrant_normalized = (angle_end_normalized / QUADRANT_ANGLE) % 4;
if (starting_quadrant_normalized == ending_quadrant_normalized) {
// Both indicate same quadrant...
config.start_quadrant.angle = angle_start_normalized;
config.start_quadrant.quadrant = radius_quadrants[starting_quadrant_normalized];
config.end_quadrant.angle = angle_end_normalized;
config.end_quadrant.quadrant = radius_quadrants[ending_quadrant_normalized];
if (angle_end - angle_start > QUADRANT_ANGLE) {
// Full quadrants:
config.full_quadrants = prv_get_full_quadrants(starting_quadrant_normalized,
ending_quadrant_normalized);
}
} else {
// Angles in different quadrants
config.start_quadrant.angle = angle_start_normalized;
config.start_quadrant.quadrant = radius_quadrants[starting_quadrant_normalized];
config.end_quadrant.angle = angle_end_normalized;
config.end_quadrant.quadrant = radius_quadrants[ending_quadrant_normalized];
if (angle_start % QUADRANT_ANGLE == 0) {
starting_quadrant_normalized--;
}
// Full quadrants
config.full_quadrants = prv_get_full_quadrants(starting_quadrant_normalized,
ending_quadrant_normalized);
}
return config;
}
static void prv_fill_oval_precise(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius_outer_x, Fixed_S16_3 radius_outer_y,
Fixed_S16_3 radius_inner_x, Fixed_S16_3 radius_inner_y,
int32_t angle_start, int32_t angle_end) {
// Drawing config calculation
const EllipsisDrawConfig config = prv_calc_draw_config_ellipsis(angle_start, angle_end);
// This flag will skip calculation of angles
const bool is_full_circle = config.full_quadrants == GCornersAll;
// This will indicate special line in the middle of the circle, when center of the circle
// lies in between lines
const bool odd_line = (center.y.fraction == FIXED_S16_3_HALF.raw_value);
// This will prevent rounding error from breaking the scanline when angles are on same side but
// in reversed order (scanline first hits end angle then hits start angle
const bool ignore_close_angles = (angle_end - angle_start > (TRIG_MAX_ANGLE / 2));
// Clipping insets to prevent negative values
radius_inner_x.raw_value = MAX(radius_inner_x.raw_value, 0);
radius_inner_y.raw_value = MAX(radius_inner_y.raw_value, 0);
// This flag prevents from calculation of the inner circle (and bugs related to it)
const bool no_inner_ellipsis = (radius_inner_x.raw_value == 0 || radius_inner_y.raw_value == 0);
// Squared radiuses values - they're used a lot in some cases
const uint32_t radius_outer_x_sq =
(radius_outer_x.raw_value * radius_outer_x.raw_value) >> FIXED_S16_3_PRECISION;
const uint32_t radius_outer_y_sq =
(radius_outer_y.raw_value * radius_outer_y.raw_value) >> FIXED_S16_3_PRECISION;
const uint32_t radius_inner_x_sq =
(radius_inner_x.raw_value * radius_inner_x.raw_value) >> FIXED_S16_3_PRECISION;
const uint32_t radius_inner_y_sq =
(radius_inner_y.raw_value * radius_inner_y.raw_value) >> FIXED_S16_3_PRECISION;
// Intersection points of angles and radiuses
GPointPrecise start_top =
prv_get_rotated_precise_point_for_ellipsis(center,
radius_outer_x.raw_value, radius_outer_y.raw_value,
config.start_quadrant.angle);
GPointPrecise end_top =
prv_get_rotated_precise_point_for_ellipsis(center,
radius_outer_x.raw_value, radius_outer_y.raw_value,
config.end_quadrant.angle);
GPointPrecise start_bottom = (no_inner_ellipsis) ? center :
prv_get_rotated_precise_point_for_ellipsis(center,
radius_inner_x.raw_value, radius_inner_y.raw_value,
config.start_quadrant.angle);
GPointPrecise end_bottom = (no_inner_ellipsis) ? center :
prv_get_rotated_precise_point_for_ellipsis(center,
radius_inner_x.raw_value, radius_inner_y.raw_value,
config.end_quadrant.angle);
// Swapping top/bottom offset points if necessary
if (start_top.y.raw_value > start_bottom.y.raw_value) {
prv_swap_precise_points(&start_top, &start_bottom);
} else if (start_top.y.raw_value == start_bottom.y.raw_value &&
(config.start_quadrant.quadrant & GCornersBottom)) {
// Special case to make bottom edge to be on left side and keep masking algorithm happy
prv_swap_precise_points(&start_top, &start_bottom);
}
if (end_top.y.raw_value > end_bottom.y.raw_value) {
prv_swap_precise_points(&end_top, &end_bottom);
} else if (end_top.y.raw_value == end_bottom.y.raw_value &&
(config.end_quadrant.quadrant & GCornersBottom)) {
// Special case to make bottom edge to be on left side and keep masking algorithm happy
prv_swap_precise_points(&end_top, &end_bottom);
}
// Range for scanline, since scanlines are mirrored from the middle of the circle this is also
// indicated from the middle, therefore initialised with 0 (as middle) and
// radius_y (as scanlines are on y axis)
int draw_min = 0;
int draw_max = radius_outer_y.integer;
// Adjust to drawing_box offset
int adjusted_center = center.y.integer + ctx->draw_state.drawing_box.origin.y;
// We add one to compensate in case of odd line needs to be drawn
int adjusted_top = adjusted_center - radius_outer_y.integer - 1;
int adjusted_bottom = adjusted_center + radius_outer_y.integer + 1;
// Clip_box
GRect clip_box = ctx->draw_state.clip_box;
// Clip adjusted values by clip_box
adjusted_top = MAX(adjusted_top, clip_box.origin.y);
adjusted_top = MIN(adjusted_top, clip_box.origin.y + clip_box.size.h);
adjusted_bottom = MAX(adjusted_bottom, clip_box.origin.y);
adjusted_bottom = MIN(adjusted_bottom, clip_box.origin.y + clip_box.size.h);
// Remove offset from adjusted values
adjusted_top -= ctx->draw_state.drawing_box.origin.y;
adjusted_bottom -= ctx->draw_state.drawing_box.origin.y;
// Calculate distances from the middle of the circle (discard negative values)
int draw_max_top = MAX(center.y.integer - adjusted_top, 0);
int draw_max_bottom = MAX(adjusted_bottom - center.y.integer, 0);
int draw_min_top = MAX(center.y.integer - adjusted_bottom, 0);
// In case of odd line, center is with half pixel so we have to subtract one more full line
int draw_min_bottom = MAX(adjusted_top - center.y.integer - 1, 0);
// Apply clipped distances
draw_max = MIN(draw_max, MAX(draw_max_top, draw_max_bottom));
draw_min = MAX(draw_min, MAX(draw_min_top, draw_min_bottom));
// Scanline offset in precise point for calculation of edges
Fixed_S16_3 y = (Fixed_S16_3){.integer = draw_min};
// Flags used for filling of solid parts of the circle, when angles are not intersecting them
const bool draw_top = is_full_circle || (config.full_quadrants & GCornersTop ||
config.start_quadrant.quadrant & GCornersTop ||
config.end_quadrant.quadrant & GCornersTop);
const bool draw_bottom = is_full_circle || (config.full_quadrants & GCornersBottom ||
config.start_quadrant.quadrant & GCornersBottom ||
(config.end_quadrant.quadrant & GCornersBottom &&
config.end_quadrant.angle % QUADRANT_ANGLE != 0));
// Offsets for mirroring of scanline (they differ for even and odd height of the circle)
int special_line_offset_top = 1;
int special_line_offset_bottom = 1;
// Color hack
const GColor stroke_color = ctx->draw_state.stroke_color;
ctx->draw_state.stroke_color = ctx->draw_state.fill_color;
// In case of odd line in the middle, this is where we draw it:
if (odd_line) {
int16_t starting_edge = center.x.raw_value - radius_outer_x.raw_value;
int16_t ending_edge = center.x.raw_value + radius_outer_x.raw_value;
if (!is_full_circle) {
// Following code finds crossing points slightly above and below center point
// as the radial angles will appear only with one of these cases, other
// will return same starting/ending point, which might be invalid angle
// We might also reject this data if delta of start/end angle is bigger than 180°
Fixed_S16_3 y_middle = (Fixed_S16_3){.raw_value = y.raw_value - 4};
prv_get_angles_mask_edge(y_middle, center, config.start_quadrant.quadrant,
&starting_edge, &ending_edge, start_top, start_bottom);
prv_get_angles_mask_edge(y_middle, center, config.end_quadrant.quadrant,
&ending_edge, &starting_edge, end_top, end_bottom);
// Now we use only one with valuable data - same starting/ending is rejected
if (starting_edge == ending_edge) {
y_middle.integer++;
prv_get_angles_mask_edge(y_middle, center, config.start_quadrant.quadrant,
&starting_edge, &ending_edge, start_top, start_bottom);
prv_get_angles_mask_edge(y_middle, center, config.end_quadrant.quadrant,
&ending_edge, &starting_edge, end_top, end_bottom);
}
}
int16_t outer_edge = prv_get_ellipsis_border(y, radius_outer_y_sq, radius_outer_x_sq).raw_value;
int16_t left = center.x.raw_value - outer_edge;
int16_t right = center.x.raw_value + outer_edge;
if (!no_inner_ellipsis && radius_inner_y.integer != 0) {
// This complicates the situation
int16_t inner_edge =
prv_get_ellipsis_border(y, radius_inner_y_sq, radius_inner_x_sq).raw_value;
int16_t inner_left = center.x.raw_value - inner_edge;
int16_t inner_right = center.x.raw_value + inner_edge;
prv_draw_scanline_collision_points(ctx, center.y.integer, left, inner_left,
starting_edge, ending_edge, ignore_close_angles);
prv_draw_scanline_collision_points(ctx, center.y.integer, inner_right, right,
starting_edge, ending_edge, ignore_close_angles);
} else {
prv_draw_scanline_collision_points(ctx, center.y.integer, left, right,
starting_edge, ending_edge, ignore_close_angles);
}
// After drawing the line we move scanline edge calculation offset
y.integer++;
} else {
// If theres no line in the middle, we move edge calculation offset by half (to stay
// in the middle of the pixel) and change bottom offset to zero (to evenly mirror lines)
y.fraction = 4;
special_line_offset_bottom = 0;
}
// Main drawing loop
for (int i=draw_min; i < draw_max; i++, y.integer++) {
// Separate min/max offsets for angles masking for lines drawn above and below center point
int16_t top_starting_edge = center.x.raw_value - radius_outer_x.raw_value;
int16_t top_ending_edge = center.x.raw_value + radius_outer_x.raw_value;
int16_t bottom_starting_edge = top_starting_edge;
int16_t bottom_ending_edge = top_ending_edge;
// If the circle is not full - calculate mask for angles
if (!is_full_circle) {
prv_get_angles_mask_edge(y, center, config.start_quadrant.quadrant,
&top_starting_edge, &bottom_ending_edge, start_top, start_bottom);
prv_get_angles_mask_edge(y, center, config.end_quadrant.quadrant,
&top_ending_edge, &bottom_starting_edge, end_top, end_bottom);
}
// Calculate outer radius edges
int16_t outer_edge = prv_get_ellipsis_border(y, radius_outer_y_sq, radius_outer_x_sq).raw_value;
int16_t left = center.x.raw_value - outer_edge;
int16_t right = center.x.raw_value + outer_edge;
// If theres circle in the middle - calculate it:
if (!no_inner_ellipsis && i < radius_inner_y.integer) {
int16_t inner_edge =
prv_get_ellipsis_border(y, radius_inner_y_sq, radius_inner_x_sq).raw_value;
int16_t inner_left = center.x.raw_value - inner_edge;
int16_t inner_right = center.x.raw_value + inner_edge;
// Using top/bottom flags we make sure to not draw outside of given angles range,
// we also draw separately left and right side of the circle
if (draw_top) {
prv_draw_scanline_collision_points(ctx, center.y.integer - i - special_line_offset_top,
left, inner_left, top_starting_edge, top_ending_edge,
ignore_close_angles);
prv_draw_scanline_collision_points(ctx, center.y.integer - i - special_line_offset_top,
inner_right, right, top_starting_edge, top_ending_edge,
ignore_close_angles);
}
if (draw_bottom) {
prv_draw_scanline_collision_points(ctx, center.y.integer + i + special_line_offset_bottom,
left, inner_left, bottom_starting_edge,
bottom_ending_edge, ignore_close_angles);
prv_draw_scanline_collision_points(ctx, center.y.integer + i + special_line_offset_bottom,
inner_right, right, bottom_starting_edge,
bottom_ending_edge, ignore_close_angles);
}
} else {
// If theres nothing in the middle, draw top and bottom parts of the circle
if (draw_top) {
prv_draw_scanline_collision_points(ctx, center.y.integer - i - special_line_offset_top,
left, right, top_starting_edge, top_ending_edge,
ignore_close_angles);
}
if (draw_bottom) {
prv_draw_scanline_collision_points(ctx, center.y.integer + i + special_line_offset_bottom,
left, right, bottom_starting_edge, bottom_ending_edge,
ignore_close_angles);
}
}
}
// Finish color hack
ctx->draw_state.stroke_color = stroke_color;
}
void prv_fill_oval_in_rect(GContext *ctx, GRect rect, uint16_t inset_x, uint16_t inset_y,
int32_t angle_start, int32_t angle_end) {
// Cast to precision point
GPointPrecise center = GPointPrecise(((rect.origin.x << FIXED_S16_3_PRECISION) +
(rect.size.w << FIXED_S16_3_PRECISION) / 2),
((rect.origin.y << FIXED_S16_3_PRECISION) +
(rect.size.h << FIXED_S16_3_PRECISION) / 2));
Fixed_S16_3 radius_outer_x = (Fixed_S16_3){.raw_value =
((rect.size.w << FIXED_S16_3_PRECISION) / 2)};
Fixed_S16_3 radius_outer_y = (Fixed_S16_3){.raw_value =
((rect.size.h << FIXED_S16_3_PRECISION) / 2)};
Fixed_S16_3 radius_inner_x = (Fixed_S16_3){.raw_value =
(((rect.size.w - inset_x * 2) << FIXED_S16_3_PRECISION) / 2)};
Fixed_S16_3 radius_inner_y = (Fixed_S16_3){.raw_value =
(((rect.size.h - inset_y * 2) << FIXED_S16_3_PRECISION) / 2)};
prv_fill_oval_precise(ctx, center, radius_outer_x, radius_outer_y,
radius_inner_x, radius_inner_y, angle_start, angle_end);
}
void prv_fill_oval(GContext *ctx, GPoint center, uint16_t outer_radius_x, uint16_t outer_radius_y,
uint16_t inner_radius_x, uint16_t inner_radius_y,
int32_t angle_start, int32_t angle_end) {
const GPointPrecise center_precise = (GPointPrecise) {
.x.integer = center.x, .x.fraction = FIXED_S16_3_HALF.raw_value,
.y.integer = center.y, .y.fraction = FIXED_S16_3_HALF.raw_value,
};
const Fixed_S16_3 outer_x_precise = (Fixed_S16_3){
.integer = outer_radius_x,
.fraction = FIXED_S16_3_HALF.raw_value
};
const Fixed_S16_3 outer_y_precise = (Fixed_S16_3){
.integer = outer_radius_y,
.fraction = FIXED_S16_3_HALF.raw_value
};
const Fixed_S16_3 inner_x_precise = (Fixed_S16_3){
.integer = inner_radius_x,
.fraction = FIXED_S16_3_HALF.raw_value
};
const Fixed_S16_3 inner_y_precise = (Fixed_S16_3){
.integer = inner_radius_y,
.fraction = FIXED_S16_3_HALF.raw_value
};
prv_fill_oval_precise(ctx, center_precise, outer_x_precise, outer_y_precise,
inner_x_precise, inner_y_precise, angle_start, angle_end);
}
void prv_fill_oval_quadrant_precise(GContext *ctx, GPointPrecise point,
Fixed_S16_3 outer_radius_x, Fixed_S16_3 outer_radius_y,
Fixed_S16_3 inner_radius_x, Fixed_S16_3 inner_radius_y,
GCornerMask quadrant) {
// Translate quadrants to angles
if (quadrant == GCornerNone) {
return;
}
// Calculate quadrants...
int32_t angle_start = 0;
int32_t angle_end = 0;
if (quadrant == GCornersAll) {
angle_end = TRIG_MAX_ANGLE;
} else {
if (quadrant & radius_quadrants[0]) {
for (int i=QUADRANTS_NUM - 1; i >= 0; i--) {
if (!(quadrant & radius_quadrants[i])) {
angle_start = ((i + 1) % 4) * (TRIG_MAX_ANGLE / 4);
break;
}
}
} else {
for (int i=1; i <= QUADRANTS_NUM; i++) {
if (quadrant & radius_quadrants[i % 4]) {
angle_start = i * (TRIG_MAX_ANGLE / 4);
break;
}
}
}
if (!(quadrant & radius_quadrants[0])) {
for (int i=QUADRANTS_NUM - 1; i >= 0; i--) {
if (quadrant & radius_quadrants[i]) {
angle_end = ((i + 1) % 4) * (TRIG_MAX_ANGLE / 4);
break;
}
}
} else {
for (int i=1; i <= QUADRANTS_NUM; i++) {
if (!(quadrant & radius_quadrants[i % 4])) {
angle_end = i * (TRIG_MAX_ANGLE / 4);
break;
}
}
}
}
if (angle_end <= angle_start) {
angle_end += TRIG_MAX_ANGLE;
}
prv_fill_oval_precise(ctx, point, outer_radius_x, outer_radius_y,
inner_radius_x, inner_radius_y, angle_start, angle_end);
}
void prv_fill_oval_quadrant(GContext *ctx, GPoint point,
uint16_t outer_radius_x, uint16_t outer_radius_y,
uint16_t inner_radius_x, uint16_t inner_radius_y,
GCornerMask quadrant) {
// Cast to precision point
const GPointPrecise center_precise = (GPointPrecise) {
.x.integer = point.x, .x.fraction = FIXED_S16_3_HALF.raw_value,
.y.integer = point.y, .y.fraction = FIXED_S16_3_HALF.raw_value,
};
const Fixed_S16_3 outer_x_precise = (Fixed_S16_3){
.integer = outer_radius_x,
.fraction = FIXED_S16_3_HALF.raw_value
};
const Fixed_S16_3 outer_y_precise = (Fixed_S16_3){
.integer = outer_radius_y,
.fraction = FIXED_S16_3_HALF.raw_value
};
const Fixed_S16_3 inner_x_precise = (Fixed_S16_3){
.integer = inner_radius_x,
.fraction = FIXED_S16_3_HALF.raw_value
};
const Fixed_S16_3 inner_y_precise = (Fixed_S16_3){
.integer = inner_radius_y,
.fraction = FIXED_S16_3_HALF.raw_value
};
prv_fill_oval_quadrant_precise(ctx, center_precise, outer_x_precise, outer_y_precise,
inner_x_precise, inner_y_precise, quadrant);
}
MOCKABLE void graphics_draw_arc_precise_internal(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius,
int32_t angle_start, int32_t angle_end) {
uint16_t stroke_width = ctx->draw_state.stroke_width;
// We accept only .0 and .5 precision for now:
center.x.raw_value -= center.x.raw_value % (FIXED_S16_3_ONE.raw_value / 2);
center.y.raw_value -= center.y.raw_value % (FIXED_S16_3_ONE.raw_value / 2);
// To maintain compatibility we have to adjust from integral points where given point means
// center of the point
center.x.raw_value += 4;
center.y.raw_value += 4;
radius.raw_value += 4;
// Same for radius:
radius.raw_value -= radius.raw_value % (FIXED_S16_3_ONE.raw_value / 2);
if (stroke_width < 1 || angle_start > angle_end) {
// Dont draw anything
return;
}
// TODO: Adapt this to support new precision, for now this is faked by regular fill
// This will be done with PBL-24777
/*
else if (stroke_width == 1) {
prv_circle_arc_draw_1px(ctx, center, radius, angle_start, angle_end);
return;
}
*/
// Color hack to draw using stroke_color instead of fill_color
GColor tmp_color = ctx->draw_state.fill_color;
ctx->draw_state.fill_color = ctx->draw_state.stroke_color;
Fixed_S16_3 half_stroke_width =
(Fixed_S16_3){.raw_value = (ctx->draw_state.stroke_width << FIXED_S16_3_PRECISION) / 2};
Fixed_S16_3 radius_inner =
(Fixed_S16_3){.raw_value = MAX(0, radius.raw_value - half_stroke_width.raw_value)};
Fixed_S16_3 radius_outer =
(Fixed_S16_3){.raw_value = radius.raw_value + half_stroke_width.raw_value};
if (radius_outer.integer > 0) {
prv_fill_oval_precise(ctx, center, radius_outer, radius_outer, radius_inner, radius_inner,
angle_start, angle_end);
if (half_stroke_width.integer >= 1) {
GPointPrecise starting_point =
prv_get_rotated_precise_point(center, radius.raw_value, angle_start);
GPointPrecise ending_point =
prv_get_rotated_precise_point(center, radius.raw_value, angle_end);
prv_fill_oval_precise(ctx, starting_point, half_stroke_width, half_stroke_width,
FIXED_S16_3_ZERO, FIXED_S16_3_ZERO, 0, TRIG_MAX_ANGLE);
prv_fill_oval_precise(ctx, ending_point, half_stroke_width, half_stroke_width,
FIXED_S16_3_ZERO, FIXED_S16_3_ZERO, 0, TRIG_MAX_ANGLE);
}
}
// Restore color
ctx->draw_state.fill_color = tmp_color;
}
void graphics_draw_arc_internal(GContext *ctx, GPoint center, uint16_t radius, int32_t angle_start,
int32_t angle_end) {
// We're just casting this to precise points
GPointPrecise fixed_center;
// GPointPreciseFromGPoint doesnt work for unit tests (!!!)
fixed_center.x.integer = center.x;
fixed_center.y.integer = center.y;
fixed_center.x.fraction = 0;
fixed_center.y.fraction = 0;
Fixed_S16_3 fixed_radius = (Fixed_S16_3){.integer = radius, .fraction = 0};
graphics_draw_arc_precise_internal(ctx, fixed_center, fixed_radius, angle_start, angle_end);
}
void graphics_draw_arc(GContext *ctx, GRect rect, GOvalScaleMode scale_mode,
int32_t angle_start, int32_t angle_end) {
GPointPrecise center;
Fixed_S16_3 radius;
grect_polar_calc_values(&rect, scale_mode, &center, &radius);
graphics_draw_arc_precise_internal(ctx, center, radius, angle_start, angle_end);
}
MOCKABLE void graphics_fill_radial_precise_internal(GContext *ctx, GPointPrecise center,
Fixed_S16_3 radius_inner, Fixed_S16_3
radius_outer, int32_t angle_start,
int32_t angle_end) {
// This function is going to be replaced with function that will support drawing of ellipsis
// as documented in PBL-23640
// For now we only accept .0 and .5 radius precision
radius_inner.raw_value -= radius_inner.raw_value % (FIXED_S16_3_ONE.raw_value / 2);
radius_outer.raw_value -= radius_outer.raw_value % (FIXED_S16_3_ONE.raw_value / 2);
// Same goes for coordinates of center point:
center.x.raw_value -= center.x.raw_value % (FIXED_S16_3_ONE.raw_value / 2);
center.y.raw_value -= center.y.raw_value % (FIXED_S16_3_ONE.raw_value / 2);
// Move the values to match old precision with integral coordinate between pixels
center.x.raw_value += 4;
center.y.raw_value += 4;
radius_inner.raw_value += 4;
radius_outer.raw_value += 4;
if (angle_start > angle_end || radius_outer.raw_value < radius_inner.raw_value) {
// Nothing will be drawn...
return;
}
// TODO: Adapt this to support new precision, for now this is faked by regular fill
// This will be done with PBL-24777
/*
if (radius_outer.raw_value - radius_inner.raw_value == FIXED_S16_3_ONE.raw_value) {
// Color hack
GColor tmp_color = ctx->draw_state.stroke_color;
ctx->draw_state.stroke_color = ctx->draw_state.fill_color;
// prv_circle_arc_draw_1px(ctx, center, radius_outer, angle_start, angle_end);
// Restore color
ctx->draw_state.stroke_color = tmp_color;
// We're done here
return;
} else
*/
if (radius_outer.raw_value - radius_inner.raw_value < FIXED_S16_3_ONE.raw_value) {
// Abort, abort!
return;
}
prv_fill_oval_precise(ctx, center, radius_outer, radius_outer,
radius_inner, radius_inner, angle_start, angle_end);
}
void graphics_fill_radial_internal(GContext *ctx, GPoint center, uint16_t radius_inner,
uint16_t radius_outer, int32_t angle_start, int32_t angle_end) {
// Cast given values to precise point
GPointPrecise center_fixed;
center_fixed.x.integer = center.x;
center_fixed.y.integer = center.y;
center_fixed.x.fraction = 0;
center_fixed.y.fraction = 0;
Fixed_S16_3 radius_inner_fixed = (Fixed_S16_3){.integer = radius_inner, .fraction = 0};
Fixed_S16_3 radius_outer_fixed = (Fixed_S16_3){.integer = radius_outer, .fraction = 0};
prv_fill_oval_precise(ctx, center_fixed, radius_outer_fixed, radius_outer_fixed,
radius_inner_fixed, radius_inner_fixed, angle_start, angle_end);
}
void graphics_fill_radial(GContext *ctx, GRect rect, GOvalScaleMode scale_mode,
uint16_t inset_thickness,
int32_t angle_start, int32_t angle_end) {
GPointPrecise center;
Fixed_S16_3 radius_outer;
grect_polar_calc_values(&rect, scale_mode, &center, &radius_outer);
const Fixed_S16_3 radius_inner =
Fixed_S16_3(radius_outer.raw_value - inset_thickness * FIXED_S16_3_ONE.raw_value);
graphics_fill_radial_precise_internal(ctx, center, radius_inner, radius_outer,
angle_start, angle_end);
}
void graphics_fill_oval(GContext *ctx, GRect rect, GOvalScaleMode scale_mode) {
const int32_t inset_thickness = MAX(ABS(rect.size.h), ABS(rect.size.w));
// Fill radial doesn't mind overlarge inset thickness
graphics_fill_radial(ctx, rect, scale_mode, inset_thickness, 0, TRIG_MAX_ANGLE);
}
void graphics_fill_circle(GContext* ctx, GPoint p, uint16_t radius) {
PBL_ASSERTN(ctx);
if (ctx->lock) {
return;
}
if (radius == 0) {
// Filling a circle of radius zero should just draw a single pixel.
// Backup the stroke color and set that to the current fill color since the stroke color
// is what is used for draw pixel. Restore the stroke color afterwards.
GColor backup_stroke_color = ctx->draw_state.stroke_color;
ctx->draw_state.stroke_color = ctx->draw_state.fill_color;
graphics_draw_pixel(ctx, p);
ctx->draw_state.stroke_color = backup_stroke_color;
return;
}
#if PBL_COLOR
if (ctx->draw_state.antialiased) {
graphics_internal_circle_quadrant_fill_aa(ctx, p, radius, GCornersAll);
return;
}
#endif
graphics_circle_fill_non_aa(ctx, p, radius);
}
GPointPrecise gpoint_from_polar_precise(const GPointPrecise *precise_center,
uint16_t precise_radius, int32_t angle) {
const uint32_t normalized_angle = normalize_angle(angle);
const GPointPrecise precise_perimeter_point = prv_get_rotated_precise_point(*precise_center,
precise_radius,
normalized_angle);
return precise_perimeter_point;
}
GPoint gpoint_from_polar_internal(const GPoint *center, uint16_t radius, int32_t angle) {
if (!center) {
return GPointZero;
}
const GPointPrecise precise_center = GPointPreciseFromGPoint((*center));
const uint16_t precise_radius = (uint16_t)(radius << GPOINT_PRECISE_PRECISION);
const GPointPrecise result = gpoint_from_polar_precise(&precise_center, precise_radius,
angle);
return GPointFromGPointPrecise(result);
}
GPointPrecise prv_gpointprecise_from_polar(GRect *rect, GOvalScaleMode scale_mode,
int32_t angle) {
GPointPrecise center;
Fixed_S16_3 radius;
grect_polar_calc_values(rect, scale_mode, &center, &radius);
return gpoint_from_polar_precise(&center, radius.raw_value, angle);
}
GPoint gpoint_from_polar(GRect rect, GOvalScaleMode scale_mode, int32_t angle) {
const GPointPrecise result = prv_gpointprecise_from_polar(&rect, scale_mode, angle);
return GPointFromGPointPrecise(result);
}
GRect grect_centered_internal(const GPointPrecise *center, GSize size) {
size.w = ABS(size.w);
size.h = ABS(size.h);
const int16_t FIXED_HALF = FIXED_S16_3_HALF.raw_value;
return (GRect) {
// Adding 0.5 to x and y here will ensure we have rounded up when we throw away the fraction
.origin.x = (center->x.raw_value - (size.w * FIXED_HALF) + FIXED_HALF) >> FIXED_S16_3_PRECISION,
.origin.y = (center->y.raw_value - (size.h * FIXED_HALF) + FIXED_HALF) >> FIXED_S16_3_PRECISION,
.size = size,
};
}
GRect grect_centered_from_polar(GRect rect, GOvalScaleMode scale_mode, int32_t angle, GSize size) {
const GPointPrecise center = prv_gpointprecise_from_polar(&rect, scale_mode, angle);
return grect_centered_internal(&center, size);
}
void grect_polar_calc_values(const GRect *r, GOvalScaleMode scale_mode, GPointPrecise *center,
Fixed_S16_3 *radius) {
if (!r) {
return;
}
GRect rect = *r;
grect_standardize(&rect);
const int16_t FIXED_ONE = FIXED_S16_3_ONE.raw_value;
const int16_t FIXED_HALF = FIXED_S16_3_HALF.raw_value;
if (radius) {
int16_t side;
switch (scale_mode) {
case GOvalScaleModeFitCircle:
side = grect_shortest_side(rect);
break;
case GOvalScaleModeFillCircle:
side = grect_longest_side(rect);
break;
default:
WTF;
}
const int16_t radius_value = ((side + 1) * FIXED_ONE) / 2 - FIXED_ONE;
*radius = Fixed_S16_3(MAX(0, radius_value));
}
if (center) {
// origin + (origin + len)/2 - 0.5 == (origin * 2 + len) / 2 - 0.5
const int16_t x = (rect.size.w <= 0) ? rect.origin.x * FIXED_ONE :
(((2 * rect.origin.x + rect.size.w) * FIXED_ONE) / 2 - FIXED_HALF);
const int16_t y = (rect.size.h <= 0) ? rect.origin.y * FIXED_ONE :
(((2 * rect.origin.y + rect.size.h) * FIXED_ONE) / 2 - FIXED_HALF);
*center = (GPointPrecise) {.x = Fixed_S16_3(x), .y = Fixed_S16_3(y)};
}
}